Testing against the database

There are a lot of test scenarios where we want to make sure our code is storing items in the database correctly. We could use the Laravel helper functions in our tests to work through transactions to keep the database clean. Because of course you don't want data from your tests to end up in the database. 

But I would still recommend working with a separate database each time. You can achieve that in two ways, either you create an extra database in your docker setup to test against, or you work in SQLite.

In this post, we are going to talk mostly about SQLite. Personally, I find the threshold a little lower to get started with it and it's faster to set up.

Testen in SQLite

The two major strengths of SQLite testing are in the speed of execution and setup. So you don't have to set up an additional database for testing. Because testing on a production database is something you want to avoid at all costs.

Laravel also makes it extremely easy for us to get started with it. In this blog post I will explain in a few steps how we run our tests in memory. So we save both time during development and time in setting up the project.

How can I test in SQLite?

Phpunit.xml

<php>
    <server name="APP_ENV" value="testing"/>
    <server name="DB_CONNECTION" value="sqlite"/>
    <server name="DB_DATABASE" value=":memory:"/>
</php>

Since you are recreating the database each time during testing, it is also best to use the following trait.

use RefreshDatabase;

This will reset the database every time and apply all your migrations, more information can be found in the documentation of testing.

What do I test, how do I test?

Don't test what you don't own.

Once you get started with testing you may quickly wonder just what all needs to be tested.
Of course we want to test as much as possible but you also have to be productive.

The first thing I want to mention is the saying: "Don't test what you don't own". 
If you use a package for some purposes, you should not test whether the package does its job properly. 
A good package has its own tests for that purpose. There is practically no point in confirming it. 

If you still want to test whether your code follows its actions properly on the basis of code you did not write yourself then you will have to use Mocking. For example, when you call an external API in your application you don't want to execute it during your tests. So you could mock the call and see if your code handles the different responses correctly.

We also use mocking to make sure that Events and Jobs are not triggered during testing. We don't want to send out emails to customers during testing !

Mocking is very extensive and has a steep learning curve. We won't go into it too much in this post, but below you can see an example of how to 'mock' an Event.


 

<?php

namespace Tests\Feature;

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function test_orders_can_be_shipped()
    {
        Event::fake();

        // Perform order shipping...

        // Assert that an event was dispatched...
        Event::assertDispatched(OrderShipped::class);

        // Assert an event was dispatched twice...
        Event::assertDispatched(OrderShipped::class, 2);

        // Assert an event was not dispatched...
        Event::assertNotDispatched(OrderFailedToShip::class);

        // Assert that no events were dispatched...
        Event::assertNothingDispatched();
    }
}






 

Code Coverage

100% or nothing?

De code coverage van een project

Through PHPUnit and Xdebug we can have a code coverage report generated. There are a lot of options, but I can especially recommend the html variant. In the following screenshot you can see what that looks like.

Of course, you want to capture as much of your code as possible in your tests. But it is not always realistic to aim for complete and thus 100% coverage. You also want to test only what you have written yourself. 
By excluding the right files and some functions you can achieve 100% coverage.

The big advantage of looking at code coverage is that you can see if your tests run over the entire codebase.
So if you validate if you are getting the correct values back on an api endpoint you can see if your test is running over the different scenarios or not.

That's why I personally think it's very important to keep an eye on your code coverage. It goes hand in hand with writing good tests.
 

Factories


Since you have no test data in a fresh sql database we are going to fill it with fake data.
For that we will use the Factories in Laravel.

Writing a factory is like most things in Laravel a simple process.

By default the following factory is provided. The UserFactory:

<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

So you define via $model = User::class the Eloquent model for which you create a factory.
Then in the definition function you pass in an array of values. Here we use the Faker library to generate some random property values.

Afterwards you need to check if you have imported the HasFactory trait into your model.
You can add it to the top of your class with:

use HasFactory;

The code snippet below is a summary of the most common functions you need to create Eloquent instances. We use the factory() method, with the count() or times() function we can create multiple models at once, we will get back a Collection of Eloquent models.

Note that through the make() function these are not saved to your database, you must use the create() method for this.

//Create a user, but don't save it yet
$user = User::factory()->make();

//Create multiple users, again we're not saving them yet.
$users = User::factory()->count(10)->make();

//Create a user and save them immediately
$user = User::factory()->create();

 

Example

So a simple feature test that tests an end-point of our API might look like this:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\Project;


class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_returns_a_project_on_show(): void
    {
        //Creating a Project
        $project = Project()->factory()->create();
        //GET api/projects with the project ID we just created

        $response = $this->getJson('api/projects/' . $project->id)->assertStatus(200);

        //Do multiple assertions with $response to ensure we get the correct data.
        self::assertSame($project->id, $response['attributes']['id']);
    }
}

 

Conclusie

So testing in a separate database is very quick to set up in Laravel.
As you can see, a lot of effort has gone into making it as pleasant as possible for the developer, which I think is typical for Laravel.

You can quickly write a whole bunch of correct tests where you can be sure that they will test the application properly.
There is a steep learning curve though. Once you have mastered the basics of testing, you can write tests for code that handles external processes. By using mocking we can really test anything. 

By selectively marking out your code, you can achieve a nice percentage of code coverage and step by step make sure your project is fully tested.

Writing tests will reduce the amount of time you have to spend manually checking your application and will save you a lot of time!