Basically I have to write tests for many Laravel Controllers most of which are CRUD (read, store, update) and most of the logic is placed inside those(Inherited code, not mine).
What I need to do is automate the testing from a User's perspective. So I need to hit all the endpoints and test against a real database and check if everything turns out well.
I have almost no experience in testing, but from what I gather controllers should be tested with integration / acceptance tests. Now I did fine with testing Read methods by extending Laravel's TestCase, here is one example :
class SongsTest extends TestCase
{
public function testBasicIndex()
{
$arguments = [];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
/**
* #dataProvider providerSearchQuery
*/
public function testSearchIndex($query)
{
$arguments = ['srquery' => $query];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
public function providerSearchQuery()
{
return array(
array('a'),
array('as%+='),
array('test?Someqsdag8hj$%$')
);
}
public function testGetSongsById()
{
$id = 1;
$response = $this->call('GET', '/songs/' . $id);
$this->assertContains($response->getStatusCode(), [200, 404]);
$this->seeJson();
if($response->getStatusCode() == 404)
{
$content = json_decode($response->getContent());
$this->assertContains($content->message[0], ['Song not found', 'Item not active']);
}
}
}
These tests hit the endpoints and check if the response is 200 and the format is JSON and few other things. These work fine.
What I have problem with is :
Let's say for example we have a UserController, and a method that creates users. After that, said user should be used in TokensController to create a token that should be somehow remembered and used in future tests with token protected requests.
My question :
How do I automate : tests of UserController's store method by creating a real user in a testing database(without mocking), tests of TokensController's store method by using that user's email, testing other controllers with the created token and deleting that once the test is done, so it can be performed once again.
I just cannot conceptualize all that since I haven't really done much testing.
This is an example to use token and user's data for testing -
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class PostTest extends TestCase
{
use WithoutMiddleware;
public $token = "lrSFMf0DpqRAh4BXTXWHp5VgFTq4CqA68qY3jG2CqvcpNTT6m0y9Qs6OdpSn";
/*
A browser that receives a 302 response code HAS to redirect which means it will take the URL in the response and submit a new request. The result you see in your browser is the redirected page.
Unit testing does not redirect. Your unit test is only doing what you direct it to do. If your unit test should test for the redirect then you evaluate the response and the correct assertion is 302 and not 200.
*/
public function testExample()
{
$this->assertTrue(true);
}
public function testLogin()
{
$this->visit('/')
->type('abc#gmail.com', 'email')
->type('123456', 'password')
->press('Login') // type submit - value / button - lable
->seePageIs('/Wall'); // for redirect url
}
public function testFavourite()
{
$this->testLogin();
$request = [
'user_id' => '172',
'token' => $this->token,
'post_id' => '500'
];
$response = $this->call('POST', '/DoFavoriteDisc',$request);
$this->assertEquals(200, $response->getStatusCode());
}
}
Hope this will help you.
Related
I created my feature test;
ProfilesControllerTest.php
<?php
namespace Tests\Feature;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ProfileControllerTest extends TestCase
{
use RefreshDatabase;
/**
* A basic feature test example.
*
* #return void
*/
public function test_the_profile_page_is_rendered()
{
// First The user is created
$user = User::factory()->create();
//act as user
$this->actingAs($user);
// Then we want to make sure a profile page is created
$response = $this->get('/profile/{user}');
//
$response->assertStatus(200);
}
}
web.php
Route::get('/profile/{user}', 'ProfilesController#index')->name('profiles.show');
But it keeps returning an error. I Suspect it is because of the profile link, however I am unsure of how to show it. I have attempted a few variations and i have not managed to get it to work.
I realised factory was not working so instead I tried this;
ProfilesControllerTest.php
public function test_the_profile_page_is_rendered()
{
// First The user is created
$user = User::make([
'name' => 'John Doe',
'username' => 'johnnyd',
'email' => 'johndoe#email.com'
]);
//act as user
$this->actingAs($user);
// Then we want to make sure a profile page is created
$response = $this->get('/profile/{$user}');
$response->assertStatus(200);
}
And I kept getting the error:
Error
php artisan test
PASS Tests\Unit\ExampleTest
✓ basic test
PASS Tests\Unit\UserTest
✓ login form
✓ user duplication
PASS Tests\Feature\ExampleTest
✓ basic test
FAIL Tests\Feature\ProfileControllerTest
✕ the profile page is rendered
Tests: 1 failed, 4 passed, 1 pending
Expected status code 200 but received 404. Failed asserting that 200 is identical to 404.
at tests/Feature/ProfileControllerTest.php:34
30| // Then we want to make sure a profile page is created
31| $response = $this->get('/profile/{$user');
32|
33| //
> 34| $response->assertStatus(200);
35| }
36| }
37|
Profile controller for index was written as follows:
ProfilesController.php
class ProfilesController extends Controller
{
public function index(User $user)
{
$postCount = Cache::remember(
'count.posts.' . $user->id,
now()->addSeconds(30),
function () use ($user) {
return $user->posts->count();
}
);
return view('profiles.index', compact('user', 'postCount'));
}
}
Your first test isn't working because you're attempting to access the wrong URL. You're attempting to go to http://localhost/profile/{user}. That URL is not correct, as there is no user with an id of "{user}". The URL you want to access is http://localhost/profile/1, to see the profile of the user with id 1.
To fix the first test, fix the URL:
// bad
// $response = $this->get('/profile/{user}');
// good
$response = $this->get('/profile/'.$user->id);
Your second test is failing for two reasons:
User::make() will make a new instance of the User model, but it will not persist anything to the database. Since the User won't exist in the database, it does not have a profile URL you can visit.
Again, as in the first test, the profile URL you're trying to visit is wrong.
So, go back to the first test, correct the URL, and you should be good.
I have a login issue when performing tests on some controller for an API with Symfony 5.2.
All my endpoints are behind a firewall, and to test everything works fine, I need to login before I make a request.
I use a JWT authentication system with https://github.com/lexik/LexikJWTAuthenticationBundle
So I have this private method on my test class :
protected function logInAsUser(string $username): User
{
$tokenStorage = static::$client->getContainer()->get('security.token_storage');
$firewallName = 'api_area';
$userRepository = static::$container->get(UserRepository::class);
$user = $userRepository->findOneBy(['username' => $username]);
$token = new PostAuthenticationGuardToken($user, $firewallName, ['ROLE_USER']);
$tokenStorage->setToken($token);
return $user;
}
Then in my test methods, I call this method before making a request on a protected endpoint, like this :
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode());
}
And it works fine
(without calling $this->logInAsUser('my-username); I got a 401 response as intended)
But when I try to make two requests in the same test method, the second one fails with a 401, and the error message is : JWT Token not found
Example:
public function testSomething() {
$this->logInAsUser('my-username');
static::$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // OK
static::$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, static::$client->getResponse()->getStatusCode()); // FAILURE
}
I tried to re login before the second call, but it doesn't change anything.
Since Symfony 5.1 the Client used in WebTestcases provides a built-in method to mock a logged in user:
public function loginUser($user, string $firewallContext = 'main'): self
You should try using that one instead of doing it yourself.
public function testSomething()
{
$client = static::createClient();
$testUser = $userRepository->findOneByUsername('my-username');
$client->loginUser($testUser);
$client->request('GET', '/api/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // OK
$client->request('GET', '/api/another/protected/endpoint/');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); // FAILURE
}
I am working on a SilverStripe project. I am writing functional tests for my unit test. Following is the scenario I am trying to test. When a POST request is made, I save the data from the request body into the SilverStripe session. I want to assert/ test that the data are stored in the session.
This is my controller class
class CustomFormPageController extends PageController
{
private static $allowed_actions = [
'testPostRequest',
];
private static $url_handlers = [
'testPostRequest' => 'testPostRequest',
];
public function testPostRequest(HTTPRequest $request)
{
if (! $request->isPOST()) {
return "Bad request";
}
//here I am saving the data in the session
$session = $request->getSession();
$session->set('my_session_key', $request->getBody());
return "Request successfully processed";
}
}
Following is my test class
class CustomFormPageTest extends FunctionalTest
{
protected static $fixture_file = 'fixtures.yml';
public function testTestingPost()
{
$formPage = $this->objFromFixture(CustomFormPage::class, 'form_page');
$formPage->publish("Stage", "Live");
$response = $this->post($formPage->URLSegment . '/testPostRequest', [
'name' => 'testing'
]);
$request = Injector::inst()->get(HTTPRequest::class);
$session = $request->getSession();
$sessionValue = $session->get('my_session_key');
var_dump($sessionValue);
}
}
When I run the test, I get the following error.
ArgumentCountError: Too few arguments to function SilverStripe\Control\HTTPRequest::__construct(), 0 passed and at least 2 expected
How can I fix it? How can I test if the data are stored in the session?
I tried this too and it always returns NULL
var_dump($this->session()->get('my_session_key');
The error you get happens when you ask Injector for the current request before it has created one. This is because FunctionalTest nests the Injector state in which it executes tests.
You can still access the FunctionalTest session using $this->session(), as you've noted.
The main reason your test is failing is because your fixtured page is not published, and FunctionalTest operates in the live stage by default (I'm assuming this, because you didn't post your fixture). You can use the draft stage using protected static $use_draft_site = true; in your test class, or you can publish the page in setUp() or your test before you make the POST request to it.
Your next problem is that $request->getBody() is null in your controller, so it's not setting anything.
This works, for an example:
//here I am saving the data in the session
$session = $request->getSession();
$session->set('my_session_key', $request->postVar('name'));
$response = $this->post($formPage->URLSegment . '/testPostRequest', [
'name' => 'testing'
]);
$sessionValue = $this->session()->get('my_session_key');
$this->assertSame('testing', $sessionValue);
I want to test the next method of my controller
function index(){
if(Auth::User()->can('view_roles'))
{
$roles = Role::all();
return response()->json(['data' => $roles], 200);
}
return response()->json(['Not_authorized'], 401);
}
it is already configured for authentication (tymondesigns / jwt-auth) and the management of roles (spatie / laravel-permission), testing with postman works, I just want to do it in an automated way.
This is the test code, if I remove the conditional function of the controller the TEST passes, but I would like to do a test using a user but I have no idea how to do it.
public function testIndexRole()
{
$this->json('GET', '/role')->seeJson([
'name' => 'admin',
'name' => 'secratary'
]);
}
Depends on what kind of app are you building.
A - Using Laravel for the entire app
If your using Laravel for frontend/backend, well to simulate a logged-in user you could use the awesome Laravel Dusk package, made by the Laravel team. You can check the documentation here.
This package has some helpful methods to mock login sessions amongs a lot more of other things, you can use:
$this->browse(function ($first, $second) {
$first->loginAs(User::find(1))
->visit('/home');
});
That way you hit an endpoint with a logged-in user of id=1. And a lot more of stuff.
B - Using Laravel as a backend
Now, this is mainly how I use Laravel.
To identify a user that hits an endpoint, the request must send an access_token. This token helps your app to identify the user. So, you will need to make and API call to that endpoint attaching the token.
I made a couple of helper functions to simply reuse this in every Test class. I wrote a Utils trait that is being used in the TestCase.php and given this class is extended by the rest of the Test classes it will be available everywhere.
1. Create the helper methods.
path/to/your/project/ tests/Utils.php
Trait Utils {
/**
* Make an API call as a User
*
* #param $user
* #param $method
* #param $uri
* #param array $data
* #param array $headers
* #return TestResponse
*/
protected function apiAs($user, $method, $uri, array $data = [], array $headers = []): TestResponse
{
$headers = array_merge([
'Authorization' => 'Bearer ' . \JWTAuth::fromUser($user),
'Accept' => 'application/json'
], $headers);
return $this->api($method, $uri, $data, $headers);
}
protected function api($method, $uri, array $data = [], array $headers = [])
{
return $this->json($method, $uri, $data, $headers);
}
}
2. Make them available.
Then in your TestCase.php use the trait:
path/to/your/project/tests/TestCase.php
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, Utils; // <-- note `Utils`
// the rest of the code
3. Use them.
So now you can do API calls from your test methods:
/**
* #test
* Test for: Role index
*/
public function a_test_for_role_index()
{
/** Given a registered user */
$user = factory(User::class)->create(['name' => 'John Doe']);
/** When the user makes the request */
$response = $this->apiAs($user,'GET', '/role');
/** Then he should see the data */
$response
->assertStatus(200)
->assertJsonFragment(['name' => 'admin'])
->assertJsonFragment(['name' => 'secretary']);
}
Side note
check that on top of the test methods there is a #test annotation, this indicates Laravel that the method is a test. You can do this or prefix your tests names with test_
I am working with some unit testing and i have difficult with one kind of assertions
$this->assertRedirectedTo
Here is my very basic example controller code
// example.com/form
public function postForm()
{
$data = "my data";
$this->mylogic($data);
}
private function mylogic($data){
// operations with $data ... after some logics
return redirect()
->route('Gracias')
->send();
}
And this is my very basic example of my unit testing
public function REDRIRECT_TEST()
{
$this->call('POST','form'); // it works
$this->assertRedirectedTo('gracias'); // it fails
}
and I get this error
Failed asserting that Illuminate\Http\Response Object (...) is an instance of class "Illuminate\Http\RedirectResponse".
This error happen when i use redirect inside another method
private function mylogic($data){
return redirect()
->route('Gracias')
->send();
}
Any idea how to deal with assertRedirectedTo where the redirect() is inside another method?
Edited.
Well, i re-thinked everythink and divided the logic from controller, now the logic is in another class and only return the correct route to redirect to the controller and my unit test pass.
In laravel 5.7+ the methods are a little changed for the unit test.
Session::start();
$response = $this->call('POST', 'form', [
'_token' => \Session::token(),
'var' => 'value',
]);
$response->assertStatus(302);
$response->assertRedirect('gracias');