Start testing your Laravel applications
Main Thread • 19 min read
Testing has returned to the spotlight recently. Jeffrey Way started a new series on techniques for cleaner code. The first few episodes emphasized how tests must exist in order to confidently refactor your code.
This is not a new concept. Michael Feathers declares any code base without tests to be legacy in Working Effectively with Legacy Code.
As someone who spent two years on an eXtreme programming team practicing TDD every day, testing not only gave me confidence in my code, but leveled-up my programming skills.
Despite this continual push towards testing, it's rare to find a Laravel application with tests. From analytics I shared in Laravel by the Numbers less than a quarter of Laravel applications have tests.
The irony is I've never come across a developer who doesn't want to write tests.
So why aren't we testing our Laravel applications?
Over the last few months I've asked developers and clients why they don't write tests for their existing Laravel applications. The answer almost always comes back to time.
Writing tests does take time. I'm not going to claim otherwise. To create the tests, data fixtures, and mocks takes time. It's also a tedious task without immediate results.
Yet, even when given the time, we still don't write tests. This is a bit of a paradox. If we know the benefits of testing and have time to do so, then why don't we write tests?
This brings me to the next common response, we don't know where to start testing.
This comes in two forms. The first form is quite literally we don't know which test to write first. The second form is more not knowing how to write the first test.
Adam Wathan expresses this exact pain point as his motivation for Test Driven Laravel. And it's a pain point for writing tests for existing Laravel applications as well.
I focus on lowering the time barrier to testing in a separate post. Today, I want to focus on getting started with testing your Laravel applications.
Table of Contents
Since this guide is nearly 5,000 words, I added a table of contents for easier navigation, continued reading, and reference.
- How to start testing?
- Where to start testing?
- Getting started with testing
- Writing the first test
- Figuring out what to test
- Writing the second test
- Model factories
- Making database assertions
- Varying test data with Faker
- Writing strong tests
- Setting authenticated users
- Additional test cases
- Sample project on GitHub
How to start testing?
I used to tell people in my testing workshops to start with testing one of the most important pieces of their application. Didn't matter how they tested it. Could be an integration test, unit test, browser test, whatever. Just write some kind of automated test.
I knew it would be a lot of work. But I felt testing a critical piece of the application would help co-worker or manager buy-in.
Unfortunately, this approach requires overcoming both the time and skill barriers to testing. Not only would it take longer to set up these types of tests, but attempting to test the most complex part of your application would require expert level knowledge of testing.
I realize now this was a mistake. The way to get started with testing is the same way you get started with anything - small, incremental steps.
How to start testing in Laravel?
Since version 5.2, Laravel has focused heavily on testing. Now Laravel makes configuration easy, includes test cases and assertions out of the box, and offers built-in ways to mock core components.
When it comes to the question of how to start to testing your Laravel applications, the answer is, hands down, HTTP Tests.
With HTTP Tests, it's simple to create any type of request to GET
, POST
, PUT
, etc. You can send request data, set an authenticated user, and set session and header data using a fluent API.
The returned response object has assertion methods to test common behavior. You can verify the HTTP status, the returned view, the view, session, or header data set, or a redirection occurred.
HTTP Tests are the easiest way for you to send a request to your application and make assertions about the response. In addition, this high-level test provides broad coverage as it touches many layers of your application including middleware, controllers, models, services, and views.
HTTP Tests give you the most return on your time investment.
Where to start testing?
So HTTP Tests answers the first half of question, there's also the question of where to start.
To the point of small, incremental steps, you should start by writing an HTTP tests that sends a request which returns a simple response.
I know to people who have written tests this may seem silly. It is a little bit. But the value of this test is not necessarily in the coverage it provides, but the momentum it provides.
If you try to test the most complex piece of your application first, you will not have the momentum to do so. And you will give up on testing.
I don't want that to happen. Testing for me has been the single biggest improvement I made as a developer. I'll admit I don't test everything. I don't seek 100% code coverage. My primary goal when testing is feeling confident my application behaves correctly.
Getting started with testing
To demonstrate getting started with testing, I will test the Laravel authentication component using HTTP Tests. This may not be something you test very thoroughly in your Laravel applications. Maybe even at all.
But for the purposes of this guide it's the perfect thing to test. All of the necessary code can be generated. This allows us to focus on the writing the tests without having to worry about varying implementation details.
The authentication components also use many layers within Laravel. It has multiple request types. It has redirection. It has data validation. It has interaction with the database. It has user authentication.
These will allow us to incrementally learn more about testing Laravel applications. From here you can apply these practices to your own applications.
So while the auth components may not be something which brings you a lot of value in relation to testing. It does provide a lot of value in learning how to test.
Installation
To get started, I'll install a brand new Laravel (5.8) application using the Laravel installer:
1laravel new start-testing-laravel
I'll switch into the Laravel project directory and run make:auth
to generate all the authentication components:
1cd start-testing-laravel/2php artisan make:auth
From these two commands, this Laravel application has all the code for managing users, including user registration, login, logout, forgot password, and authentication.
I don't have to write any code. Which is great, since I want to focus on writing tests.
Running tests
Out of the box, Laravel includes a preconfigured test environment and example tests. Underneath it uses the PHPUnit testing framework.
To run these tests, I can call the phpunit
test runner installed by composer:
1vendor/bin/phpunit
I see these sample tests pass and everything is green.
Testing structure
Laravel stores tests under the tests
folder. Under the tests
folder, Laravel has two subfolders: Feature
and Unit
.
These terms carry with them implications about the tests. Unfortunately, this is exactly the kind of quagmire you can get stuck in when starting to test. I'm intentionally going to skirt the issue. For today, tests are tests. The only distinction I will make is placing Laravel's HTTP Tests under the Feature
subfolder as this is how the example tests are organized.
Organizing Your Tests
On the topic of subfolders, I encourage organizing your tests to mirror the app
folder. So if you are testing the app/Http/Controllers/UserController.php
, you should create the test as tests/Feature/Http/Controllers/UserControllerTest.php
. Mirroring the folder structures avoids confusion on where (or if) a test exists.
Writing the first test
For the first test, I want to start with something very small. The goal is to gain momentum with testing. I don't want to have to do a lot of configuration, setup, or learn about mocking objects.
Start by testing something simple. This will vary based on your application. An example within the current application might be testing the main page or the login page.
I'll choose writing a test for the login page and build from there. I can generate a new test class with the make:test
command:
1php artisan make:test Http/Controllers/Auth/LoginControllerTest
Let's quickly review this command. First, the path mirrors that of my app
folder. Second, the suffix of Test
. Any PHP file within the Feature
or Unit
folders with the Test
suffix will automatically be run by PHPUnit. While this is configurable, it's a common convention and one Laravel follows out of the box.
This command generates the following class:
1<?php 2 3namespace Tests\Feature\Http\Controllers\Auth; 4 5use Tests\TestCase; 6use Illuminate\Foundation\Testing\WithFaker; 7use Illuminate\Foundation\Testing\RefreshDatabase; 8 9class LoginControllerTest extends TestCase10{11 /**12 * A basic feature test example.13 *14 * @return void15 */16 public function testExample()17 {18 $response = $this->get('/');1920 $response->assertStatus(200);21 }22}
There's a few important things to note. First, this class extends the TestCase
class. This is located in the tests
folder and where you may add code to share across your test suite. It in turn extends Laravel's TestCase
class which provides helper methods and assertions you may use during testing.
Second, it created a test case. By convention, any public
function prefixed with test
within a TestCase
class will be run by PHPUnit. You may also use an @test
annotation to mark a test case. I find using this annotation with a snake case name to be more common when writing tests for Laravel applications.
I will follow this convention and adjust the test name to relay what our test is attempting to verify.
1- /**2- * A basic feature test example.3- *4- * @return void5- */6+ /** @test */7- public function testExample()8+ public function login_displays_the_login_form()
This name may seem redundant. But as your test suite grows it will help provide that extra bit of context needed when fixing a failing test.
Figuring out what to test
Now that I created the test case, I need to actually write the test. It's important to focus on the behavior this test aims to verify. In this case, I want to ensure when I request /login
it display the login form.
I can then translate this high level goal into a more technical language. Again, don't worry about writing the test. Focus on what you know. You know the code.
So in code, this means:
When I send a
GET
request to thelogin
route,
Then it should return theauth.login
view.
Armed with this technical language (Gherkin), I only need to fill in the blanks. I already see from the generated HTTP Test how to make a request. I will need to change the route. I also see I can make assertions on the response. By browsing the TestResponse class, I see all available assertions and find assertViewIs
.
I'll make these changes to the test case:
1/** @test */2public function login_displays_the_login_form()3{4 $response = $this->get(route('login'));5 6 $response->assertStatus(200);7 $response->assertViewIs('auth.login');8}
I'll run the tests again, but this time limit it to the LoginControllerTest
by passing the path to the test class:
1vendor/bin/phpunit tests/Feature/Http/Controllers/Auth/LoginControllerTest.php
The test passes and everything is green.
Now I know this test may not seem that valuable. That's okay. The value is not about the confidence the test provides. The value is in the confidence it provides about testing. You wrote your first Laravel test. Allow that little dopamine to take hold and use it to write the next test.
Writing the second test
Now that we have our first test, let's tackle something a bit more complex. We're not ready to test integrations yet. But maybe we can test some other kind of behavior related to making basic requests.
Sticking with the login form, let's actually make a request which submits the form data and attempts to log in. Since we're not yet ready to integrate with the database, we can test the login responded with a login error.
Let's translate this goal once more into technical language:
When I make a
POST
request to thelogin
URI,
Given I have sent invalid credentials,
Then I am redirected back to the login page,
And I receive a validation error.
First, I'll write the initial test case:
1/** @test */2public function login_displays_validation_errors()3{4 // ...5}
Next, I'll fill in the test case to send the request and assert the response behaved as expected.
I sent GET
request before. To send a POST
request we simply call post()
instead. Looking at the post()
method signature we see it accepts additional arguments, with the second being the request data.
1post(string $uri, array $data = [], array $headers = []);
I can change code for assertStatus
to 302
to verify the redirection.
Now I need to assert the validation errors. I know Laravel puts validation errors in the session. Taking another look at the response assertions we find assertSessionHasErrors()
. I can use this to verify the session contains validation errors for certain form fields.
Putting this all together, the test case becomes:
1/** @test */2public function login_displays_validation_errors()3{4 $response = $this->post('/login', []);5 6 $response->assertStatus(302);7 $response->assertSessionHasErrors('email');8}
Different Redirection Behavior
You may be inclined to write an assertion to verify the response was redirect to the login
route using assertRedirect(route('login'))
. While this is the expected behavior, this assertion would fail. This is because Laravel uses back()
for its redirection. Since these requests are sent without a referrer, they will always be redirected back to the root url. If you want to set the referrer, you may chain the from()
method before your request.
Testing is about confidence
You might feel the validation test we wrote is a bit incomplete. I didn't pass any data. I didn't assert the exact validation message. I didn't write test cases for other combinations of invalid data.
When getting started with testing you may be inclined to write test cases for every code path. That's fine. Especially if it helps you gain momentum. But ultimately testing is about confidence, not coverage.
I rarely test every possible path through the code. I only test enough paths to give me confidence the code is behaving as expected. After this, testing additional code paths don't provide much more confidence.
This single test case gives me enough confidence sending invalid data to login
behaves as expected. Especially since Laravel only returns a validation error for the email
field. This is a security measure to not expose information that could be used as an attack vector, such as a valid email, but an invalid password. So any combination of invalid data would perform the same assertions.
Of course, this is a tradeoff. If there is a unique code path, by all means test it. If you later find a bug in the code, add a test case for it. But don't worry about writing every test for every code path all at once.
Testing Validation Alternatives
If you are using Form Requests, I wrote about an alternative way to test validation which maximized coverage while minimizing the number of tests you need to write.
Model factories
I'm ready to test logging into the application. But in order to do so I need to have a user in the database which matches the credentials sent to /login
.
This brings us to the next incremental step in testing our Laravel applications. Since Laravel is an MVC framework it's very likely our code uses Models and, more broadly, Eloquent.
So how do we test Eloquent? The answer is we don't. Instead, we put data in the database and allow Laravel to behave as it would normally.
That may sound rather intimidating, but Laravel makes this easy to setup with a few simple steps.
First, I can create a factory for our model. This allows us to generate a model and pre-fill its data quickly. These factories are located underneath the database/factories
folder.
Laravel comes with a UserFactory
out of the box:
1<?php 2 3/** @var \Illuminate\Database\Eloquent\Factory $factory */ 4 5use App\User; 6use Illuminate\Support\Str; 7use Faker\Generator as Faker; 8 9$factory->define(User::class, function (Faker $faker) {10 return [11 'name' => $faker->name,12 'email' => $faker->unique()->safeEmail,13 'email_verified_at' => now(),14 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password15 'remember_token' => Str::random(10),16 ];17});
Once this is defined, we can use the factory()
helper within any of our test cases to create a model and persist it to the database.
I'll write the initial test case for a successful request to login by creating a valid user.
1/** @test */2public function login_authenticates_and_redirects_user()3{4 $user = factory(User::class)->create();5 6 // ...7}
I need to do a few more things to configure this tests to use the database.
First, I will configure it to use an SQLite in-memory database. I find this to be more performant and avoids conflicting with my development database. Using SQLite is not required. Similar to your application database, you may configure your tests to use any database you prefer.
In the end, set these in your project's phpunit.xml
file.
1<php> 2 <server name="APP_ENV" value="testing"/> 3 <server name="BCRYPT_ROUNDS" value="4"/> 4 <server name="CACHE_DRIVER" value="array"/> 5 <server name="MAIL_DRIVER" value="array"/> 6 <server name="QUEUE_CONNECTION" value="sync"/> 7 <server name="SESSION_DRIVER" value="array"/> 8+ <server name="DB_CONNECTION" value="sqlite"/> 9+ <server name="DB_DATABASE" value=":memory:"/>10</php>
I also need to tell this test to use the database. Otherwise, when I go to run the test will receive several database errors similar to this:
This is because although I configured the database, it hasn't been created. All I need to do is add the RefreshDatabase
trait to my test class.
1class LoginControllerTest extends TestCase2{3+ use RefreshDatabase;
This trait will run the application's database migrations and between each test case refresh the database to its original state.
So for any test case I only have to create the data necessary to yield the expected behavior.
Going back to the login test case, it ends up looking pretty similar to the validation test case. I use a new assertion to verify the authenticated user is the same as the user we created by the factory()
. This is provided by Laravel's TestCase
.
1/** @test */ 2public function login_authenticates_and_redirects_user() 3{ 4 $user = factory(User::class)->create(); 5 6 $response = $this->post(route('login'), [ 7 'email' => $user->email, 8 'password' => 'password' 9 ]);10 11 $response->assertRedirect(route('home'));12 $this->assertAuthenticatedAs($user);13}
Making database assertions
Creating data for our application to use is one side of the coin. The other side is asserting our application created data.
To emphasize this let's write a test case for user registration. I'll focus on the happy path. That is the path where the code behaves without error. For user registration, that is creating a new user with the registration data.
I can apply what we know so far to start writing most of this test case by sending a POST
request to registration with valid data and confirming redirection to the home
route.
1/** @test */ 2public function register_creates_and_authenticates_a_user() 3{ 4 $response = $this->post('register', [ 5 'name' => 'JMac', 7 'password' => 'password', 8 'password_confirmation' => 'password', 9 ]);10 11 $response->assertRedirect(route('home'));12 13 // ...14}
While this test would pass, it doesn't completely verify the expected behavior. There are two more aspects we haven't covered.
First, an assertion to verify the user was created in the database with the request data. To do this we can use another one of Laravel's TestCase
assertions: assertDatabaseHas
. The method accepts a few parameters.
1assertDatabaseHas(string $table, array $data, string $connection = null)
For this test case, I want to check the users
table contains a record matching the name
and email
sent as the request data.
To do so, I'll add the following assertion.
1$this->assertDatabaseHas('users', [2 'name' => 'JMac',4]);
Varying test data with Faker
To start, I hardcoded values to send to the register request. But given the opportunity I like to vary my test data.
Laravel has a development dependency for the Faker package. Faker has a rich API for generating all sorts of common data.
I can decorate any test class with a faker
property by adding the WithFaker
trait provided by Laravel.
1class LoginControllerTest extends TestCase2{3- use RefreshDatabase;4+ use RefreshDatabase, WithFaker;
Now I can update the test case to vary the request data using Faker.
1/** @test */ 2public function register_creates_and_authenticates_a_user() 3{ 4 $name = $this->faker->name; 5 $email = $this->faker->safeEmail; 6 $password = $this->faker->password(8); 7 8 $response = $this->post('register', [ 9 'name' => $name,10 'email' => $email,11 'password' => $password,12 'password_confirmation' => $password,13 ]);14 15 $response->assertRedirect(route('home'));16 17 $this->assertDatabaseHas('users', [18 'name' => $name,19 'email' => $email20 ]);21}
Don't Go Overboard
You may be tempted to do this all the time. However, varying data is not a requirement. I have done so here to demonstrate using Faker rather than a practice which should always be used. Take the time to fake data when it boosts confidence. Otherwise, hardcoded values are fine.
Writing strong tests
While the test case passes I still haven't tested the user was authenticated. It happens implicit by the redirection to the homepage, but it should be explicit. Again, I want to feel confident the test case confirms the expected behavior.
I could use another TestCase
assertion to verify the user is authenticated with $this->assertAuthenticated()
. This is okay. What would be better is to assert the authenticated user is the same user created during registration.
I don't have a reference to the user that was created. Only the data. But, I can retrieve it using Eloquent within the test case.
In doing so, I can then add the same assertion I used in my login test case. This completes the test case and gives me full confidence registration is behaving as expected.
Retrieving the user also confirms the user was indeed saved and removes the need for using $this->assertDatabaseHas()
. Adding this assertion and refactoring yields:
1/** @test */ 2public function register_creates_and_authenticates_a_user() 3{ 4 $name = $this->faker->name; 5 $email = $this->faker->safeEmail; 6 $password = $this->faker->password(8); 7 8 $response = $this->post('register', [ 9 'name' => $name,10 'email' => $email,11 'password' => $password,12 'password_confirmation' => $password,13 ]);14 15 $response->assertRedirect(route('home'));16 17 $user = User::where('email', $email)->where('name', $name)->first();18 $this->assertNotNull($user);19 20 $this->assertAuthenticatedAs($user);21}
Sanity Checks
You may have noticed the assertNotNull()
. This is what some call a sanity check. It's a simple assertion which verifies any setup within the test case behaves as expected. After all, tests are code too, and prone to mistakes.
Setting authenticated users
Using what we've learned so far will get your pretty far in testing your Laravel applications. But there's one last bit of setup which you will need to know - being able to set the authenticated user for a request.
To do so, simply prefix your request chain with the actingAs()
method and pass it the authenticated user.
I'll demonstrate this with a simple test case for the home
route which is behind the auth
middleware.
1<?php 2namespace Tests\Feature\Http\Controllers; 3use App\User; 4use Illuminate\Foundation\Testing\RefreshDatabase; 5use Tests\TestCase; 6class HomeControllerTest extends TestCase 7{ 8 use RefreshDatabase; 9 /** @test */10 public function index_returns_a_view()11 {12 $user = factory(User::class)->create();1314 $response = $this->actingAs($user)->get(route('home'));1516 $response->assertStatus(200);17 }18}
Additional test cases
While testing this behavior within your Laravel application may not be something you always do, it's a great way to get started with testing Laravel. If this was your first time testing Laravel, I encourage you to practice what you learned here by writing some of the missing test cases.
These include:
- Validation for the
/registration
data - Access
/home
without an authenticated user
Sample project on GitHub
The Laravel application, all of the test cases, as well as the additional test cases are available on GitHub. The commit history contains atomic commits for each step of this post. Feel free to use it as a reference to follow along or browse the final code.
Need to test your Laravel applications? Check out the Tests Generator Shift to quickly generate tests for an existing codebase and the Confident Laravel video course for a step-by-step guide from no tests to a confidently tested Laravel application.