Laravel testing tips & tricks
Improve tests for your laravel application
Introduction
PHPUnit is a unit testing framework for the PHP programming language. Laravel has support for PHPUnit included out of the box, and a phpunit.xml
file is already set up for your application.
In your project your tests
directory will contain 2 directories: Feature
and Unit
. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Tests within your "Unit" test directory do not boot your Laravel application and therefore are unable to access your application's database or other framework services.
Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint. Generally, most of your tests should be feature tests. These types of tests provide the most confidence that your system as a whole is functioning as intended.
Set up
By default Laravel will use your database settings from your .env
file. To get started quickly you should be able to override these settings in the phpunit.xml
file by adding the following configurations:
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
This will run your tests in an ‘in memory’ sqlite database.
Running tests
An ExampleTest.php file is provided in both the Feature and Unit test directories. After installing a new Laravel application, execute the command vendor/bin/phpunit
or php artisan test
to run your tests.
Normally tests will be run sequentially, however since January 2021, you can now execute tests in parallel, often vastly reducing the time it takes to run tests.
To do this, ensure you are running the latest version of Collision and Laravel 8:
composer update nunomaduro/collision laravel/framework
Then include the --parallel
option when executing the test
command.
php artisan test --parallel
Flags
Any flags that can be passed to the phpunit
command can also be passed to the Artisan test
command.
--filter
This will filter the tests to run. Tests can be filtered by method name or class.
--testsuite
This will filter the testsuites to run. To find out the testsuite options you can run: --list-suites
.The default options are Unit and Feature.
--stop-on-failure
This will stop the test execution upon the first error or failure. There are many other flags, but we can’t go through them all in this article. To see the list of options run: --/vendor/bin/phpunit --help
Creating a new test
To create a new test case, use the make:test Artisan command:
php artisan make:test UserTest
By default, tests will be placed in the tests/Feature
directory. To place a test within the tests/Unit
directory you may use the --unit
flag.
Auth
actingAs
Sets the currently logged in user for the application. If you want to act as a certain type of user pass a user object into this method.
$this->actingAs(User::factory()->create());
Assert
Asserts usually come towards the end of your test and they just mean ‘check this is what I expect it to be’.
assertDatabaseHas
Assert that the record which should have been created has indeed been created.
assertInstanceOf
Assert that the object is of a given type:
$this->assertInstanceOf(Collection::class, $user->posts);
assertRedirect
Assert whether the response is redirecting to a given URI. After a post
is created there is a redirect in my controller: return redirect('/posts');
I can test the redirect is working using assertRedirect
:
$this->post('posts', $attributes)->assertRedirect('/posts');
This is checking that, after my post
has been created, that it will redirect to the posts
endpoint.
assertStatus, assertJsonStructure, assertJsonFragment
Assert that the response has the status code ‘200’, assert that the json response has the given structure, and assert that the json response contains the given json fragment.
$this->json('GET', '/posts')->assertStatus(200)->assertJsonStructure(['data' => ['*' => ['id','title']]])->assertJsonFragment(['title' => 'Example post']);
assertSee
Assert that the given string or array of strings are contained within the response. For example:
$this->get('/posts')->assertSee($attributes['title']);
Here I assert that when I retrieve the posts from my posts
endpoint they will have a title.
assertSessionHasErrors
Assert that the request validation is returning the correct errors. For example, say I wan’t to ensure all my posts
have a title:
public function test_a_post_requires_a_title()
{
$this->post('/posts', [])->assertSessionHasErrors('title');
}
Here I am passing an empty array to the posts endpoint — simulating a request without a title
property. Then I am asserting that I will see an error because I don't have a title for the post.
You can find a list of phpunit assertions here: https://phpunit.readthedocs.io/en/9.5/assertions.html
Using faker
Faker is a useful library to generate fake data quickly and easily https://github.com/fzaninotto/Faker. For example say you would like a real sounding name, address, company name you can simply say faker->name
and it will pick one at random.
Now if we wat to pull this library into our test we can simply add a use
statement at the top of our class, and simply call the relevant faker method:
<?phpnamespace Tests\\Feature;use Illuminate\\Foundation\\Testing\\WithFaker;
use Tests\\TestCase;class PostsTest extends TestCase
{
use WithFaker;public function a_user_can_create_a_post()
{
$attributes = [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph,
];$this->post('posts', $attributes);$this->assertDatabaseHas('posts', $attributes);
}
}
Model Factories
When testing, you may need to insert a few records into your database before executing your test. Instead of manually specifying the value of each column when you create this test data, Laravel allows you to define a set of default attributes for each of your Eloquent models using model factories.
Creating a new model factory
To create a new factory, run the artisan make
command:
php artisan make:factory PostFactory --model=Post
The new factory class will be placed in your database/factories
directory.
The --model
option may be used to indicate the name of the model created by the factory.
If you are using laravel 8, your factory will have a definition
method. Older versions will have a define
method. Either way, you need to populate the return
method with the data to be generated when generating your models:
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'title' => $this->faker->sentence,
'desription' => $this->faker->paragraph,
];
}
This will use the faker
library to generate new data for title
and description
of each new Post
model created with this factory.
Now anywhere I run $post = Post::factory()->make();
I will have generated a new instance of the model. Note that the make()
method will not persist the record in the database. If you want to save to the database use create()
.
If I want to supply some of the data, and have the remaining fields populated with fake data I can do this:
$post = Post::factory()->make(['title' => 'My title']);
We saw that create
saves a record to the database, make
creates a model object but keeps it in memory but there is another useful method: raw
. The raw
method will save the model attrbutes to an array.
For example, my assertSessionHasErrors test could have been:
public function test_a_post_requires_a_title()
{
$attributes = Post::factory()->raw(['title' => '']);
$this->post('/posts', $attributes)->assertSessionHasErrors('title');
}
Rather than sending an empty array, I am now sending an empty title
, but all the other fields will be supplied and valid. This way, no other validation issues will interfere with what we are trying to test.
Disable exception handling
By default when an exception is thrown Laravel will catch it and handle it gracefully. For testing though we do not always want that to happen — sometimes we do actually want to see the exception.
To achieve this add $this->withoutExceptionHandling();
at the start of the test. This is useful, for example, if you are sending a request which is failing, but yet you wouldn't see the error until you test to see if a database record has been created. Disabling exception handling will throw the error when the request fails.
Tidy up
We want to run each test as isolated as possible and so it is important to clear things up after each test has executed.
To clear up any database records after each test we can use the trait RefreshDatabase
. Simply add the statement use Refreshdatabase;
within your class.
Conclusion
Testing Laravel applications is such a vast topic that we can only begin to scratch the surface in one article. I have tried to cover some of the more used, or under used testing methods. Hopefully this has been of some use to you — thanks for reading!