• Portretfoto van Coen van der Kamp
    Coen van der Kamp

De testpiramide

De testpiramide is een methode die gaat over de soorten tests die je kunt inzetten om te bewijzen dat software correct werkt. In dit artikel geef ik voorbeelden van de diverse soorten tests en leg ik uit waarom de piramide belangrijk is.

De testpiramide helpt om balans te vinden tussen soorten tests. Veel langzame tests maken een suite traag en kostbaar, terwijl weinig tests of tests van het verkeerde soort risico’s met zich meebrengen.

Afbeelding van de testpiramide: Unit, Integratie en UI

Scenario

Als voorbeeld kies ik een formulier voor algemene voorwaarden. Een gebruiker moet het vinkje aanzetten om akkoord te gaan; initieel moet het vinkje uit staan; het formulier is alleen geldig als het vinkje aan is.

Het formulier in Django-code:

class TermsOfServiceForm(forms.Form):
    accepted = forms.BooleanField(initial=False, required=True)

Unit test

Onderaan de testpiramide vormen unit tests de basis. Unit tests controleren of kleine stukken code correct werken. Ze hebben géén afhankelijkheden van andere onderdelen van het systeem en zeggen dus niets over het functioneren van het geheel. Omdat ze slechts een specifiek stukje code testen, geven falende tests precies aan wat er fout gaat. Dit is een unit test:

def test_terms_of_service_form():
    form = TermsOfServiceForm()
    assert form.fields["accepted"].initial is False
    assert form.is_valid() is False
    assert form.errors["accepted"] == ["This field is required."]
    
    form = TermsOfServiceForm(data={"accepted": True})
    assert form.is_valid() is True

Integratietest

In het midden van de piramide vinden we de integratietest. Deze test hoe verschillende componenten van de code samenwerken. In dit geval raakt de test de URL-, view-, form- en template-code. Een integratietest heeft afhankelijkheden, zoals het opstarten van de applicatie en het gebruik van een testclient. Dit maakt integratietests langzamer dan unittests. Als een test faalt, is de oorzaak minder duidelijk dan bij een unittest.

def test_tos_view(client, user):
    url = reverse("tos_accept_form")
    response = client.get(url, user=user)
    assert response.form["accepted"].value is False
    
    form = response.form
    response = form.submit()
    assert "This field is required" in response.text
    
    form["accepted"] = True
    response = form.submit()
    assert "Thank you" in response.text

User interface-tests

Bovenaan de piramide vinden we user interface-tests. Deze worden uitgevoerd in een echte browser en simuleren wat een eindgebruiker daadwerkelijk doet. User interface-tests zijn nog afstandelijker en geven bij een fout vaak weinig aanwijzingen over de oorzaak. Misschien denk je dat dit de meest betrouwbare test is, maar in de praktijk testen deze voornamelijk de happy flow en niet alle randgevallen. User interface-tests zijn voor ontwikkelaars het meest bewerkelijk, omdat de testcode weinig overeenkomt met de applicatiecode.

def test_terms_of_service_ui():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("http://localhost:8000/terms-of-service/")
        
        response = page.click("button[type='submit']")
        assert "This field is required" in response.content.decode()
        
        page.locator("#accepted").click()
        response = page.click("button[type='submit']")
        assert "Thank you" in response.content.decode()
        
        browser.close()

Handmatig testen

Boven de piramide hangt vaak een klein wolkje dat staat voor handmatig testen. Handmatige tests zijn zeer tijdrovend en daarom geen vast onderdeel van de test suite. Ze kunnen pas uitgevoerd worden als de werkzaamheden helemaal af zijn.

Wolkje boven de Piramide


In een test suite wil je géén handmatige tests, alleen automatische tests. Doorlopend handmatig testen zou het ontwikkelproces teveel frustreren.

Handmatig testen is wel nuttig bij de interne functionele beoordeling (quality assurance) of acceptatie-tests door de opdrachtgever. Hier wordt door mensen gekeken of wel voldaan is aan de opdracht. Dit is eenmalig bij het opleveren en accepteren van nieuwe features.

Statische analyse en linters

Statische analyse en linters scannen de code zonder deze uit te voeren. Ze zijn dus fundamenteel anders dan de reeds genoemde tests. Deze tools kunnen veelvoorkomende fouten opsporen en garanderen en correcte syntax.

Piramide: Statische analyse uitgelicht

Misschien bestonden deze tools nog niet toen de testpiramide werd bedacht, of beschouwen puristen dit niet als echte tests. Wat mij betreft horen deze tools als een dun laagje onder de piramide. Ze zijn namelijk nóg sneller en testen nóg kleinere stukken code.

Bij Four Digits draaien we deze tools ook eerst. Dit maakt dat de testsuite snel faalt en onvolkomenheden snel kunnen worden opgelost.

Wat heb je aan de piramide in de praktijk?

De testpiramide is een methode, of eigenlijk een metafoor, voor de verhouding tussen verschillende soorten tests. In een ideale situatie vertrouw je vooral op veel unittests, een aantal integratietests, weinig user interface-tests, en geen handmatige tests.

Als een project zo is opgebouwd, kun je – als de hele test suite slaagt – met vertrouwen uitrollen. Je mag er dan vanuit gaan dat je met minimale inspanning maximale zekerheid hebt. Bovendien blijft het project goed onderhoudbaar.

Dit hangt natuurlijk ook af van het type project. Een klein project heeft weinig tests nodig en zal ongeacht de soort snel genoeg draaien. Bij grote projecten met doorlopende ontwikkeling, waar de test suite wekelijks honderden keren wordt uitgevoerd, kan zelfs een minuut tijdwinst per testcyclus op termijn uren besparen.

Daarnaast zorgt een goed opgebouwde testpiramide voor snellere en duidelijkere feedback. Problemen worden sneller opgespoord en opgelost.

IJsje?

Laten we eens kijken naar een suboptimaal project. In dat geval verandert de piramide in een ijsje: de meeste tests zijn handmatig of UI-tests, en er zijn nauwelijks unittests. Dit kost veel tijd en geeft onvoldoende vertrouwen om nieuwe versies uit te rollen.

Als een livegang spannend aanvoelt, is dat een teken dat je test suite niet voldoende betrouwbaar is. Een investering in het uitbreiden en herstructureren van je tests kan dan gerechtvaardigd zijn.

Het grote plaatje

In softwareontwikkeling ligt de focus vaak op specifieke taken, waardoor het grotere plaatje soms uit beeld raakt. Toch is het belangrijk om af en toe met een helikopterblik naar je teststrategie te kijken.

Misschien heb je kritiek op mijn indeling van tests of de naamgeving. Softwareontwikkeling kent immers net zoveel termen voor tests als Eskimo’s namen hebben voor sneeuw. Maar daar gaat het niet om. Waar het wel om gaat, is dat je test suite grotendeels bestaat uit snelle tests die specifieke code controleren, met minimale afhankelijkheid van UI-tests, en dat je wegblijft van handmatig testen.

Voor kleine, kortlopende projecten is dit minder relevant. Maar bij grote projecten met een lange levensduur wil je dat je testpiramide staat als een huis.

Pak je voordeel

Veel methodes en metaforen uit softwareontwikkeling zijn ook toepasbaar op andere branches en werkprocessen. Heb je weleens nagedacht over de kwaliteitscontroles binnen jouw organisatie? En over de balans tussen de omvang van deze checks en de totaalkosten? Denk je dat een verschuiving naar kleinere, gerichtere controles die problemen vroegtijdig signaleren je kan helpen efficiënter te werken? Of vertrouw je liever op een grote eindcontrole?

We love code