Er zijn een hoop test scenario's waar we zeker willen zijn of onze code items correct in de database opslaan.
We zouden de Laravel helper functies kunnen gebruiken in onze testen om via transacties te werken om zo de database clean te houden. Want natuurlijk wil je niet dat er data van je testen in de database terecht komt.
Maar ik wil toch aanraden om telkens met een aparte database te werken. Dat kan je op twee manieren bereiken, ofwel maak je in je docker setup een extra database aan om tegen te testen, ofwel werk je in SQLite.
In deze post gaan we het vooral over SQLite hebben. Persoonlijk vind ik de drempel iets lager om eraan te beginnen en is het sneller opgezet.
Testen in Laravel
Testen tegen de database
Testen in SQLite
De twee grote troeven van SQLite testen zitten in de snelheid van uitvoering en opzet.
Je hoeft dus geen extra database op te zetten om te testen. Want testen op een productie database wil je ten alle koste vermijden.
Laravel maakt het ons ook enorm gemakkelijk om er mee aan de slag te gaan. In deze blog post leg ik je in enkele stappen uit hoe we onze testen in het geheugen laten lopen. Zo winnen we dus zowel tijd tijdens het ontwikkelen als tijd in het opzetten van het project.
Hoe kan ik testen in SQLite?
Phpunit.xml
<php>
<server name="APP_ENV" value="testing"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
</php>
Omdat je telkens de database opnieuw aanmaakt tijdens het testen gebruik je ook best de volgende trait.
use RefreshDatabase;
Zo wordt de database telkens opnieuw gereset en worden al je migraties toegepast, meer informatie kan je vinden in de documentatie van testing.
Wat test ik, hoe test ik?
Don't test what you don't own.
Zodra je aan de slag gaat met testen kan je je snel afvragen wat er juist allemaal getest moet worden.
Natuurlijk willen we zoveel mogelijk testen maar je moet natuurlijk ook productief zijn.
Het eerste wat ik wil aanhalen is de spreuk: "Don't test what you don't own".
Als je een package gebruikt om het een en ander te gebruiken dan moet je niet gaan testen of de package zijn werk goed doet.
Daarvoor heeft een goede package namelijk zelf testen voor. Het heeft praktisch geen nut om dat nog eens te gaan bevestigen.
Als je toch op basis van code die je zelf niet geschreven wil testen of je code de handelingen goed opvolgt dan zal je gebruik moeten maken van Mocking. Wanneer je bv een externe API aanspreekt in je applicatie wil je die tijdens je testen niet uitvoeren. Je zou dus de call kunnen 'mocken' en kijken of je code de verschillende responses correct afhandelt.
Ook gebruiken we mocking om ervoor te zorgen dat Events en Jobs niet tijdens het testen worden getriggerd. We willen geen emails naar klanten uitsturen tijdens het testen !
Mocken is heel uitgebreid en heeft een steile leercurve. In deze post gaan we er niet teveel op ingaan, maar hieronder kan je een voorbeeld bekijken van hoe je een Event 'mocked'.
<?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?
Via PHPUnit en Xdebug kunnen we een code coverage rapport laten genereren. Er zijn een tal van opties, maar ik kan vooral de html variant aanraden. In de volgende screenshot kan je zien hoe dat er uit ziet.
Natuurlijk wil je zoveel mogelijk van je code opvangen in je testen. Maar het is niet altijd realistisch om te streven naar een volledige en dus 100% coverage. Je wilt natuurlijk ook enkel testen wat je zelf hebt geschreven.
Door middel van de juiste files te excluden en sommige functies niet mee te nemen kan je wel een 100% coverage bereiken.
Het grote voordeel van te kijken naar code coverage is dat je kan zien of je testen wel over de hele codebase lopen.
Als je dus valideert of je op een api endpoint wel de correcte waardes terug krijgt kan je zien of je test over de verschillende scenario's loopt of niet.
Daarom vind ik het persoonlijk heel belangrijk om je code coverage in het oog te houden. Het gaat hand in hand met goede testen te schrijven.
Factories
Aangezien je geen testdata hebt in een verse sql database gaan we deze vullen met fake data.
Daarvoor maken we gebruik van de Factories in Laravel.
Een factory schrijven is zoals de meeste dingen in Laravel een simpel proces.
Standaard wordt volgende factory meegegeven. De 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),
];
}
}
Je bepaalt dus via $model = User::class het Eloquent model waar je een factory voor aanmaakt.
Dan geef je in de definition functie een array mee met waarden. Hier gebruiken we de Faker library om een paar random property values te generen.
Nadien moet je in je model ook even controlleren of je de HasFactory trait hebt geimporteerd.
Die kan je bovenaan je class toevoegen met:
use HasFactory;
Onderstaande code snippet is een samenvatting van de meest gebruikte functies die je nodig hebt om Eloquent instanties aan te maken. We gebruiken de factory() methode, met de count() of times() functie kunnen we meerdere modellen tegelijk aanmaken, we krijgen dan een Collection van Eloquent modellen terug.
Let op, via de make() functie worden deze niet in je database gesaved, je moet hiervoor de create() method gebruiken.
//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();
Voorbeeld
Een simpele feature test die een end-point van onze API test kan er dus zo uitzien:
<?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
Testen in een aparte database is dus zeer snel op te zetten in Laravel.
Zoals je merkt is er flink ingezet om het zo aangenaam mogelijk te maken voor de ontwikkelaar, wat volgens mij wel typisch voor Laravel is.
Je kan snel een hele hoop correcte testen uitschrijven waar je zeker van kan zijn dat ze de applicatie goed doortesten.
Er is wel een steile leercurve. Eens je de basis van testen goed onder de knie hebt kan je testen schrijven voor code die externe processen afhandelen. Door gebruik te maken van 'mocking' kunnen we echt alles testen.
Door selectief je code af te bakenen kan je een mooi percentage van code coverage bereiken en kan je stap voor stap ervoor zorgen dat je project volledig wordt getest.
Testen schrijven zorgt ervoor dat je minder tijd moet spenderen aan het manueel controleren van je applicatie en je kan zo enorm veel tijd uit sparen!