2. Testen van code in Python
In dit hoofdstuk behandelen we het testen van code in Python. Teste helpt bij het opsporen van fouten, het waarborgen van codekwaliteit en het vergemakkelijken van onderhoud. We bespreken verschillende testmethoden, frameworks en best practices.
1. Waarom testen belangrijk is
Testen is een cruciaal onderdeel van softwareontwikkeling. Het zorgt ervoor dat je programma's goed werken en correcte resultaten produceren. Wanneer je bestaande code aanpast of uitbreidt, garanderen tests dat je geen bestaande functionaliteit onbedoeld breekt. Dit geeft je vertrouwen om wijzigingen door te voeren zonder angst voor onverwachte gevolgen.
In een team zorgen tests ervoor dat jouw aanpassingen geen negatieve impact hebben op het werk van anderen. Het creëert een veilige basis waarop iedereen kan bouwen. Steeds meer softwareprojecten gebruiken continuous integration (CI) met geautomatiseerde testfases die bij elke code-wijziging worden uitgevoerd.
Je hoeft niet alles te testen. Focus op de meest kritische aspecten van je functies en klassen – de delen waar fouten de meeste schade kunnen veroorzaken. Door tests te schrijven vóór je code (test-driven development) dwing je jezelf om na te denken over wat je functie moet doen voordat je begint met implementeren. Dit leidt tot betere, meer doordachte code.
Hoewel testen vaak minder populair is bij beginnende programmeurs, zal het bewust zijn van het belang ervan het vertrouwen in je eigen code alleen maar vergroten, vooral bij aanpassingen en uitbreidingen.
2. Pytest
Pytest is een krachtige en flexibele testtool voor Python die geschikt is voor alle soorten en niveaus van softwaretesten. Het is een testframework dat automatisch tests vindt, uitvoert en de resultaten rapporteert via de command-line. Pytest bevat een uitgebreide bibliotheek met handige functies waarmee je effectiever kunt testen. Het framework kan worden uitgebreid door zelf plugins te schrijven of plugins van derden te installeren, en integreert eenvoudig met andere tools zoals continuous integration en webautomatisering.
Pytest installeren
De documentatie van pytest vind je op https://pytest.org. Installeren doe je met pip:
pip install pytest
Werk bij voorkeur in een virtuele omgeving voor elk project om dependencies gescheiden te houden.
Een geslaagde test
Pytest herkent testfuncties automatisch als ze beginnen met test_ en in een bestand staan dat ook begint met test_. De assert statement bepaalt of de test slaagt of faalt. assert is een Python keyword dat een AssertionError opwerpt wanneer de conditie niet waar is.
# test_demo_1.py
def test_passing():
assert 1 + 1 == 2
Je voert de test uit met:
pytest test_demo_1.py
Een gefaalde test
Wanneer een test faalt, toont pytest welke verwachting niet is uitgekomen. Je kunt de verbose-flag gebruiken voor meer details:
# test_demo_2.py
def test_failing():
assert 1 + 1 == 3
pytest -v test_demo_2.py
De --tb=no flag schakelt tracebacks uit als je een beknopter overzicht wilt:
pytest --tb=no test_demo_2.py
Het proces waarbij pytest automatisch tests vindt, noemen we de "test discovery" fase. Pytest zoekt naar bestanden die beginnen met test_ en functies binnen die bestanden die ook met test_ beginnen.
Mogelijke test-uitkomsten
Pytest kent verschillende uitkomsten die aangeven wat er met een test is gebeurd:
| Uitkomst | Afkorting | Betekenis |
|---|---|---|
PASSED | . | De test is geslaagd. |
FAILED | F | De test is gefaald. |
SKIPPED | s | De test is overgeslagen. |
XFAILED | x | De test is verwacht te falen (verwachte mislukking). |
XPASSED | X | De test is onverwacht geslaagd (verwachte mislukking is niet opgetreden). |
ERROR | E | Er is een fout opgetreden tijdens het uitvoeren van de test. |
3. Functies testen
We gebruiken de volgende voorbeeldfunctie om verschillende testtechnieken te demonstreren:
# divide.py
def divide(a: float, b: float) -> float:
"""Deelt a door b en retourneert het resultaat."""
return a / b
Om te testen of deze functie correct werkt, maken we gebruiken van het keywordt argument assert:
# test_divide.py
from divide import divide
def test_divide():
assert divide(10,2) == 5
Deze test controleert of de divide functie het juiste resultaat teruggeeft wanneer 10 wordt gedeeld door 2. Als de conditie achter assert waar is, slaagt de test; anders faalt deze.
soorten assertions
Je kunt verschillende soorten assertions gebruiken om verschillende aspecten van je functies te testen:
- Gelijkheid: Controleer of twee waarden gelijk zijn.
assert divide(9,3) == 3 - Ongelijkheid: Controleer of twee waarden niet gelijk zijn.
assert divide(10,2) != 6 - Waarheid: Controleer of een conditie waar is.
Onwaarheid: Controleer of een conditie onwaar is.
assert divide(8,4) > 1assert divide(5,2) < 3 - Lidmaatschap: Controleer of een element in een collectie zit.
result = [divide(12,4)]
assert 3 in result4. Klassen testen
Naast functies kun je ook klassen testen. Stel dat we een eenvoudige Calculator klasse hebben:
# calculator.py
class Calculator:
def add(self, a: float, b: float) -> float:
return a + b
def subtract(self, a: float, b: float) -> float:
return a - b
We kunnen een testklasse maken om de methoden van Calculator te testen:
# test_calculator.py
from calculator import Calculator
def test_calculator():
calc = Calculator()
assert calc.add(5, 3) == 8
assert calc.subtract(10, 4) == 6
In deze test maken we een instantie van Calculator en gebruiken we assertions om te controleren of de add en subtract methoden de verwachte resultaten opleveren.
5. Fixtures
Fixtures in pytest zijn een krachtige manier om herbruikbare testdata of -omgevingen te creëren. Ze helpen bij het opzetten van de benodigde context voor je tests, zoals het initialiseren van objecten of het voorbereiden van databases.
Waarom fixtures gebruiken?
Zonder fixtures moet je in elke test opnieuw hetzelfde setup-werk doen:
# test_calculator_without_fixture.py
from calculator import Calculator
def test_add():
calc = Calculator() # Herhaalde setup
assert calc.add(5, 3) == 8
def test_subtract():
calc = Calculator() # Herhaalde setup
assert calc.subtract(10, 4) == 6
def test_multiply():
calc = Calculator() # Herhaalde setup
assert calc.multiply(3, 4) == 12
Dit leidt tot codeherhaling en maakt onderhoud moeilijker. Als je de initialisatie moet aanpassen, moet je dit in elke test doen.
Met fixtures centraliseer je de setup-logica:
# test_calculator_with_fixture.py
import pytest
from calculator import Calculator
@pytest.fixture
def calculator():
"""Fixture die een Calculator instantie aanmaakt voor elke test."""
return Calculator()
def test_add(calculator):
assert calculator.add(5, 3) == 8
def test_subtract(calculator):
assert calculator.subtract(10, 4) == 6
def test_multiply(calculator):
assert calculator.multiply(3, 4) == 12
Hier definieert de calculator fixture hoe een Calculator instantie wordt aangemaakt. Elke test die deze fixture als parameter accepteert, krijgt automatisch een nieuwe Calculator instantie. Dit vermindert codeherhaling en maakt het eenvoudiger om de setup aan te passen.
Belang van testen in de praktijk
Wanneer je tests toevoegt aan je projecten, verhoogt dit het respect ervoor binnen de developer community. Andere developers voelen zich comfortabeler om met je code te experimenteren en zijn meer geneigd om met je samen te werken. Tests tonen aan dat je zorgvuldig bent met je code en nadenkt over de kwaliteit ervan.
Bij bijdragen aan bestaande projecten wordt verwacht dat jouw code bestaande tests doorstaat. Het schrijven van tests voor nieuwe functionaliteiten is niet alleen gebruikelijk, maar vaak verplicht in professionele omgevingen. Pull requests zonder tests worden regelmatig afgewezen.
Begin met experimenteren om vertrouwd te raken met het testproces. Focus in eerste instantie op het schrijven van tests voor de meest kritieke gedragingen van je functies en klassen. Volledige test coverage is niet noodzakelijk in vroege projecten. Begin met wat het meest belangrijk is en bouw van daaruit verder.
Goed geteste code bouwt vertrouwen op, zowel bij jezelf als bij anderen die jouw code gebruiken of erbij betrokken raken. Tests geven zekerheid dat wijzigingen geen onverwachte problemen veroorzaken, wat refactoring en uitbreidingen veel veiliger maakt. Testen helpt ook bij het vroegtijdig identificeren en oplossen van fouten. Bugs die tijdens ontwikkeling worden gevonden zijn veel goedkoper om op te lossen dan bugs die in productie terechtkomen.
Het proces van testen is ook een leermoment. Het dwingt je om na te denken over de structuur en het ontwerp van je code. Door tests te schrijven, ontdek je vaak edge cases en scenarios die je anders over het hoofd had gezien, wat leidt tot betere en doordachtere code.