Testing with Laravel — Laravel 10 Discovery Training

In this new chapter, I propose to talk about tests and we will see how Laravel allows us to test an application. As usual, Laravel has thought things through and already includes the necessary tools for testing. The first thing we can notice is that by default, we already have a file phpunit.xml which allows to define the configuration to launch the tests via the phpunit tool. In the same way, we have the possibility with the command artisan to run the tests.

php artisan test

At the root of the project we can note the presence of a folder tests which will contain two subfolders.

  • Feature will contain the functional tests. These are tests in which you will have access to the environment of the Laravel application (Service container, Facades…) and which will allow you to test your classes in relation to the rest of the application.
  • Unit will contain unit tests where we will test our code in total isolation. These tests will use the class TestCase base of PHPUnit.

Functional tests

To test the general operation of our application, we will be able to simulate a request and make assertions on the response obtained.

<?php

namespace Tests\Feature;

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

class HomeTest extends TestCase
{
    public function test_home_page_respond_correctly(): void
    {
        $response = $this->get("https://grafikart.fr/");

        $response->assertStatus(200);
    }
}

Different assertions make it possible to verify that the expected behavior is the correct one. For example, when sending a form, you can check that the errors are indeed in sessions.

<?php

namespace Tests\Feature;

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

class ContactTest extends TestCase
{
    public function test_contact_respond_correctly(): void
    {
        $response = $this->post("https://grafikart.fr/contact", [
            'name' => 'John Doe',
            'email' => 'fake',
            'content' => 'This is a fake content'
        ]);

        $response->assertRedirect();
        $response->assertSessionHasErrors(['email']);
        $response->assertSessionHasInput('email', 'fake');
    }
}

Test with database

To initialize the database for a test it is possible to use the line RefreshDatabase

use Illuminate\Foundation\Testing\RefreshDatabase;

class ExampleTest extends TestCase
{

    use RefreshDatabase;

    public function test_with_db(): void
    {
        $response = $this->get("https://grafikart.fr/");
        $response->assertOk();
    }
}

You can then use the factories to fill your database with the data needed for the test or you can use the method seed() to fill the database using the seeders that we saw in a previous chapter.

$this->seed(OrderStatusSeeder::class);

Finally, if your objective is to verify that the action saves records in the database, you can use the assertion assertDatabaseHas.

$this->assertDatabaseCount('users', 5);
$this->assertDatabaseHas('users', [
    'email' => 'sally@example.com',
]);

Do not hesitate to take a look at the documentation to discover the assertions available.

Test with Authentication

Also if you action have limited access to certain users you can use the method actingAs() And withSession() to complete the session.

<?php

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

class DashboardTest extends TestCase
{
    public function test_dashboard(): void
    {
        $user = User::factory()->admin()->create();

        $response = $this->actingAs($user)
                         ->get('/admin');
        $response->assertOk();
    }
}

Mock a service

When your code interacts with third-party elements (such as an API for example) it is necessary to be able to simulate the different types of return. Laravel integrates Mockery which allows you to change the behavior of a class on the fly.

use App\Service\WeatherApi;
use Mockery\MockInterface;

$mock = $this->mock(WeatherApi::class, function (MockInterface $mock) {
    $mock->shouldReceive('isSunny')->once()->andReturn(false);
});

In the case of facades, it is possible to mock them directly on the Facade. This mocking also works in the unit test framework but in this case you will have to think about reinitializing in the function tearDown().

<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_get_index(): void
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $response = $this->get('/users');
    }
}

Unit tests

Unit tests work in a classic way, but there are a few subtleties.

Mockery for mocks

Laravel integrates Mockery for managing mocks.

public function test_weather(): void
{
    $mock = \Mockery::mock(WeatherApiClient::class);
    $mock->shouldReceive('isSunny')->once()->andReturn(false);
    $service = new WeatherService($mock);
    // ...
}

If your functions use facades, you can also mock them using the method we saw previously. On the other hand, it will be necessary to remember to reset them after each test.

protected function tearDown(): void
{
    parent::tearDown();
    Cache::clearResolvedInstances();
}