Start testing your Laravel applications

Main Thread July 9, 2019 • 21 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.

  1. How to start testing?
    1. How to start testing in Laravel?
  2. Where to start testing?
  3. Getting started with testing
    1. Installation
    2. Running tests
    3. Testing structure
  4. Writing the first test
  5. Figuring out what to test
  6. Writing the second test
    1. Testing is about confidence
  7. Model factories
  8. Making database assertions
  9. Varying test data with Faker
  10. Writing strong tests
  11. Setting authenticated users
  12. Additional test cases
  13. 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:

laravel new start-testing-laravel

I'll switch into the Laravel project directory and run make:auth to generate all the authentication components:

cd start-testing-laravel/
php 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:

vendor/bin/phpunit

I see these sample tests pass and everything is green.

Laravel's example tests passing

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.

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:

php 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:

<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class LoginControllerTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

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.

-    /**
-     * A basic feature test example.
-     *
-     * @return void
-     */
+    /** @test */
-    public function testExample()
+    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 the login route,
Then it should return the auth.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:

/** @test */
public function login_displays_the_login_form()
{
    $response = $this->get(route('login'));

    $response->assertStatus(200);
    $response->assertViewIs('auth.login');
}

I'll run the tests again, but this time limit it to the LoginControllerTest by passing the path to the test class:

vendor/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 the login 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:

/** @test */
public function login_displays_validation_errors()
{
    // ...
}

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.

post(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:

/** @test */
public function login_displays_validation_errors()
{
    $response = $this->post('/login', []);

    $response->assertStatus(302);
    $response->assertSessionHasErrors('email');
}

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.

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:

<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\User;
use Illuminate\Support\Str;
use Faker\Generator as Faker;

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

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.

/** @test */
public function login_authenticates_and_redirects_user()
{
    $user = factory(User::class)->create();

    // ...
}

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.

<php>
     <server name="APP_ENV" value="testing"/>
     <server name="BCRYPT_ROUNDS" value="4"/>
     <server name="CACHE_DRIVER" value="array"/>
     <server name="MAIL_DRIVER" value="array"/>
     <server name="QUEUE_CONNECTION" value="sync"/>
     <server name="SESSION_DRIVER" value="array"/>
+    <server name="DB_CONNECTION" value="sqlite"/>
+    <server name="DB_DATABASE" value=":memory:"/>
</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:

Laravel test database errors

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.

class LoginControllerTest extends TestCase
{
+    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.

/** @test */
public function login_authenticates_and_redirects_user()
{
    $user = factory(User::class)->create();

    $response = $this->post(route('login'), [
        'email' => $user->email,
        'password' => 'password'
    ]);

    $response->assertRedirect(route('home'));
    $this->assertAuthenticatedAs($user);
}

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.

/** @test */
public function register_creates_and_authenticates_a_user()
{
    $response = $this->post('register', [
        'name' => 'JMac',
        'email' => 'jmac@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertRedirect(route('home'));

    // ...
}

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.

assertDatabaseHas(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.

$this->assertDatabaseHas('users', [
    'name' => 'JMac',
    'email' => 'jmac@example.com'
]);

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.

class LoginControllerTest extends TestCase
{
-    use RefreshDatabase;
+    use RefreshDatabase, WithFaker;

Now I can update the test case to vary the request data using Faker.

/** @test */
public function register_creates_and_authenticates_a_user()
{
    $name = $this->faker->name;
    $email = $this->faker->safeEmail;
    $password = $this->faker->password(8);

    $response = $this->post('register', [
        'name' => $name,
        'email' => $email,
        'password' => $password,
        'password_confirmation' => $password,
    ]);

    $response->assertRedirect(route('home'));

    $this->assertDatabaseHas('users', [
        'name' => $name,
        'email' => $email
    ]);
}

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:

/** @test */
public function register_creates_and_authenticates_a_user()
{
    $name = $this->faker->name;
    $email = $this->faker->safeEmail;
    $password = $this->faker->password(8);

    $response = $this->post('register', [
        'name' => $name,
        'email' => $email,
        'password' => $password,
        'password_confirmation' => $password,
    ]);

    $response->assertRedirect(route('home'));

    $user = User::where('email', $email)->where('name', $name)->first();
    $this->assertNotNull($user);

    $this->assertAuthenticatedAs($user);
}

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.

<?php
namespace Tests\Feature\Http\Controllers;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HomeControllerTest extends TestCase
{
    use RefreshDatabase;
    /** @test */
    public function index_returns_a_view()
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)->get(route('home'));

        $response->assertStatus(200);
    }
}

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 Test 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.