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 classTestCase
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();
}