In deze driedelige serie geef ik een paar tips en recepten om meer uit testautomatisering te halen voor jou als developer. Soms zijn het algemene trucs, maar soms ook hele specifieke oplossingen voor een bepaald probleem. Meestal geef ik bij elke tip praktische voorbeelden met code en testcases. De code is in PHP en de voorbeeld-testcases zijn gemaakt met behulp van PHPUnit. Soms is er een stuk Laravel specifieke code toegepast. Echter, de ideeën zijn ook toepasbaar voor andere programmeertalen. Kortom, in drie delen totaal 14 tips over testautomatisering voor elke developer!

In dit eerst deel laat ik vooral zien hoe je slimmer kunt testen. In deel twee komen transactions en code coverage aan bod. Tenslotte bespreek ik in deel drie stubs, polymorphisme en fixtures. Aan het eind van deze serie schrijf je tests (en code) als nooit tevoren!

Tip 1: Test de grenswaarden

Toen ik begon met het schrijven van unit tests gebruikte ik willekeurige waarden voor variabelen. Dat dekt weliswaar alle scenarios, maar ik testte niet de grenswaarden, de plek die juist belangrijk is omdat daar makkelijk een foutje in kan sluipen. Het is dus beter om die grenswaarden op te zoeken en er omheen te testen. Bij grenswaarden-analyse wordt dus gekeken of het systeem verandert zodra de waarde van die parameter een bepaalde grens overschrijdt.

Neem als voorbeeld de acceptatiecriteria “Toon alleen de aanmeldingen van de afgelopen zeven dagen”. Hier zijn twee grenzen: Precies 7 dagen geleden en tussen gisteren en vandaag.

grenswaarden visueel weergegeven

Op basis van deze grenzen zijn kan ik de volgende scenarios opstellen:

  • Aanmelding van 8 dagen geleden wordt niet getoond
  • Aanmelding van 7 dagen geleden wordt getoond
  • Aanmelding van 6 dagen geleden wordt getoond
  • Aanmelding van gisteren wordt getoond
  • Aanmelding van vandaag wordt niet getoond

Dit leg ik vast in unit tests en hiermee voorkom ik off-by-one-errors. Ik test dus of de vergelijkingen (> of >=; < of <=) juist zijn gedaan.



Ik pas grenswaarden-analyse ook toe als ik handmatig iets aan het testen ben; bijvoorbeeld bij datumvelden of invoervelden die een getal zijn.


Tip 2: Combineer tests

Ik was laatst bezig om een bepaalde query testen waarbij geverifieerd moest worden dat het juiste resultaat werd teruggegeven. Ik schreef daarvoor dan een Integration test. De query bevatte bepaalde clausules die ik allemaal wilde testen, en zeker dat bepaalde records ook buiten set van ge-query-de records vielen. Er zijn dus meerdere scenarios. Namelijk voor elk WHERE-statement in de query twee tests: Een binnen de set, en een erbuiten.

Hier een simpel voorbeeld die min of meer de query voorstelt die ik aan het testen was: Ik haal personen uit de tabel “people” op met achternaam “Jansen”. De query is als volgt:

SELECT *
FROM people
WHERE last_name = ?

Ik stel dan twee scenarios op: Een waarbij de query een resultaat teruggeeft met een persoon die als achternaam “Jansen” heeft. Het andere scenario waarbij personen zijn die niet de achternaam “Jansen” hebben en dus ook niet teruggegeven worden.

De tabel voor deze test vul ik dan met de volgende testdata:

id voornaam tussenvoegsel achternaam
1 Jan   Jansen
2 Piet de Vries

Omdat het opzetten en uitvoeren van de scenarios gelijk zijn kan ik de scenarios samenvoegen tot één test. Ik assert ten eerste dat record met id 1 inderdaad in het resultaat van de query zit. En ook assert ik dat record met id 2 niet in het resultaat zit. De test ziet er dan zo uit:

class PeopleRepositoryTest extends IntegrationTestCase
{
    public function testCanRetrievePeopleByLastName(): void
    {
        $person1 = factory(Person::class, ['last_name' => 'Jansen']);
        $person2 = factory(Person::class, ['last_name' => 'Vries']);
        
        $repository = app()->make(PeopleRepository::class);
        
        $records = $repository->findByLastName('Jansen');
        
        $this->assertContains($person1->id, $records->keys());
        $this->assertNotContains($person2->id, $records->keys());
    }
}

Het voordeel is dus in dit simpele voorbeeld één test minder. Integration tests zijn over het algemeen niet de snelste tests dus dat is weer gewonnen tijd. Met meer clausules in de query die getest moet worden loopt het voordeel dus alleen maar op.

Tip 3: Zorg dat je tests een duidelijke opbouw hebben

De opbouw van testcases doe ik altijd op min of meer dezelfde manier. Allereerst breng ik de applicatie (of deel daarvan) in een toestand waarop ik wil testen. Dit kan bijvoorbeeld zijn door bepaalde data klaar te zetten, of bepaalde mocks klaar te zetten.

Vervolgens voer ik één actie uit, namelijk het deel van de applicatie dat ik wil testen. Bijvoorbeeld om te zien of een bepaalde berekening juist is. Ik beperk dit altijd tot één actie, en niet meerdere omdat anders de ene actie de andere zou kunnen beïnvloeden. Als de test dan niet zou slagen is het ingewikkelder om te achterhalen waar het fout is gegaan. En bovendien duurt het uitvoeren van de test ook langer.

Tenslotte assert ik dat hetgene is gedaan wat ik verwachtte dat er zou gaan gebeuren. Bijvoorbeeld dat de uitkomst van een bepaalde berekening het juiste antwoord heeft gegeven of dat er een bepaald record in de database staat.

class ExampleTest extends TestCase
{
    public function testCanDemonstrateGivenWhenThen()
    {
        // given a record in the database (arrange)
        $userId = factory(User::class)->getId();

        // when I try to find it by id (act)
        $user = $this->app
            ->make(UserRepository::class)
            ->find($userId);

        // then I expect a User (assert)
        $this->assertInstanceOf(User::class, $user);
    }
}

Bij het schrijven van acceptance tests heet dit principe Given-When-Then, maar het is ook prima toepasbaar op andere soorten tests. Dat heet dan Arrange, Act, Assert.

Tip 4: Schrijf eerst de test, dan de code

Ik zie programmeurs nog vaak eerst een hele feature programmeren om vervolgens aan het eind “nog even” de tests toe te voegen. Vaak is er dan “geen tijd meer voor”. Als dat voor jou wel werkt dan is dat natuurlijk helemaal prima, maar toch zou je kunnen overwegen om het een keer andersom te doen. Schrijf eerst de test, zie de test falen, schrijf een minimale hoeveelheid code om de test te doen slagen en ga vervolgens door met de volgende test. Op deze manier schrijf je veel bewuster je code en voorkom je dat er unreachable code ontstaat. Je zult zien dat je je meteen afvraagt “Wat als deze parameter geen integer maar een string is?”, etcetera. Hiervoor voeg je eenvoudig een testcase toe om de uitkomst vast te leggen. Dit is Test Driven Development.

Tot slot

De volgende blogpost in de serie "Haal meer uit testautomatisering" gaat over snelheid van je tests, transacties en code coverage en staat binnenkort online.

Heel veel succes! Heb je vragen of kom je er niet helemaal uit? Mail me gerust, ik help je graag!

Geschreven door: Jeroen Schouten

Meer kennis bijspijkeren? Kom dan naar onze Meetup: Ode aan de Code!

Bekijk onze Meetups

Wij zijn altijd op zoek naar getalenteerde vakgenoten!

Bekijk onze vacatures