← Python/Testen
Les 5 van 8
MODULE 10 · 05◷ 35 min

Iedere test met verse gegevens laten beginnen

Na deze les kun je
  • Een pytest-fixture via een functieparameter aanvragen.
  • Veranderlijke testgegevens per test opnieuw maken.
  • Een resource met yield en finally ook na een testfout opruimen.

Een test die apart slaagt, kan na een andere test ineens mislukken. Misschien heeft die eerdere test een gedeelde lijst veranderd. Je wilt iedere controle met bekende beginwaarden uitvoeren, zodat de volgorde geen verborgen onderdeel van de afspraak wordt.

Voorbereiding op één herkenbare plaats

Een fixture is een functie die testgegevens of een resource voorbereidt. Met @pytest.fixture meld je haar bij pytest aan. Een test vraagt haar aan door de fixturenaam als parameter te gebruiken.

De test ontvangt de gemaakte waarde. Je roept de fixture daarom niet zelf aan met notes(); pytest verzorgt het maken en doorgeven. Dat lijkt op een functieparameter die je al kent, met pytest als aanroeper.

Het eerste voorbeeld vraagt dezelfde fixture in twee tests aan. De eerste test voegt iets toe. De tweede ontvangt toch weer een lege lijst.

Twee tests gebruiken ieder een verse lijst
test_notes.py
import pytest

@pytest.fixture
def notes():
    return []

def test_add(notes):
    notes.append("idee")
    assert notes == ["idee"]

def test_empty(notes):
    assert notes == []
run_tests.py
import contextlib, io, pytest

class Recorded:
    def __init__(self):
        self.count = 0
        self.outcomes = []

    def pytest_collection_modifyitems(self, items):
        self.count = len(items)

    def pytest_runtest_logreport(self, report):
        if report.when == "call":
            self.outcomes.append(report.outcome)

recorded = Recorded()
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
    status = pytest.main(["-q", "--capture=sys", "-p", "no:cacheprovider", "--color=no", "test_notes.py"], plugins=[recorded])
print("gevonden:", recorded.count)
print("geslaagd:", recorded.outcomes.count("passed"))
print("mislukt:", recorded.outcomes.count("failed"))
print("status:", int(status))
Verwachte uitvoer
gevonden: 2
geslaagd: 2
mislukt: 0
status: 0

De levensduur bepaalt wat wordt gedeeld

De scope bepaalt hoe lang pytest een fixturewaarde bewaart. Zonder een andere scope geldt hier function: iedere test krijgt een nieuwe fixture-uitvoering. De waarde wordt binnen diezelfde test wel bewaard als meerdere onderdelen haar aanvragen.

Een nieuwe uitvoering betekent alleen een nieuw object als je dat object daar ook maakt. return [] maakt iedere keer een lijst. Een lijst buiten de fixture maken en steeds teruggeven blijft gedeelde toestand, ook met de standaard scope.

Een langere scope, zoals module, kan nuttig zijn voor dure voorbereiding, maar deelt de waarde tussen tests uit die module. Gebruik dat bewust. Voor een veranderlijke winkelmand is een nieuwe lijst per test juist de duidelijke keuze.

Niet alleen opbouwen, ook afsluiten

Sommige voorbereiding moet later worden teruggedraaid. Een yield-fixture levert haar waarde met yield en gaat na de test verder om op te ruimen. Je kent het onderbreken van een generator al uit module acht.

Zet de gebruikte resource in een try/finally-constructie. De resource wordt vóór yield geopend; de opruiming staat in finally. Wanneer de test na het ontvangen van de waarde mislukt, voert pytest de opruiming alsnog uit.

Het tweede voorbeeld veroorzaakt bewust een mislukte assertie. De gemeten gebeurtenissen laten zien dat sluit toch na gebruik komt. Een rode test hoeft dus geen vergeten resource achter te laten.

De yield-fixture ruimt ook na een mislukte assertie op
test_resource.py
import pytest
events = []

@pytest.fixture
def resource():
    events.append("open")
    try:
        yield "lokale proef"
    finally:
        events.append("sluit")

def test_resource(resource):
    events.append("gebruik")
    assert resource == "andere waarde"
run_tests.py
import contextlib, io, pytest
import test_resource

class Recorded:
    def __init__(self):
        self.count = 0
        self.outcomes = []

    def pytest_collection_modifyitems(self, items):
        self.count = len(items)

    def pytest_runtest_logreport(self, report):
        if report.when == "call":
            self.outcomes.append(report.outcome)

recorded = Recorded()
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
    status = pytest.main(["-q", "--capture=sys", "-p", "no:cacheprovider", "--color=no", "test_resource.py"], plugins=[recorded])
print("gevonden:", recorded.count)
print("geslaagd:", recorded.outcomes.count("passed"))
print("mislukt:", recorded.outcomes.count("failed"))
print("status:", int(status))
print(",".join(test_resource.events))
Verwachte uitvoer
gevonden: 1
geslaagd: 0
mislukt: 1
status: 1
open,gebruik,sluit

Dit geldt voor de resource die al is opgebouwd en geleverd. Een fout tijdens eerdere voorbereiding verdient eigen foutafhandeling. Houd daarom iedere fixture klein en geef haar één duidelijke verantwoordelijkheid.

Veelgemaakte fout: opruimen vóór het gebruik

Sluiten vóór yield geeft de test een al gesloten resource. Alleen return gebruiken laat code daaronder juist niet meer draaien. Volg de volgorde voorbereiden, leveren, opruimen en controleer haar ook wanneer de test faalt.

Een mand en een lokale proefresource

Maak in de opdracht een verse cart-fixture en gebruik haar voor toevoegen en verwijderen. De vaste module cart_ops voegt met add toe en verwijdert met take het laatste item. Een verwijdering geeft dat item terug.

De tweede fixture levert een eenvoudige box in het geheugen. Er wordt geen echt netwerk of bestand geopend. De les controleert objectidentiteit, beginwaarden en daadwerkelijk sluiten, ook bij een gecontroleerde ValueError.

AAN DE SLAG 0/4
1

Een verse lijst per test

Maak de fixture cart met @pytest.fixture. Laat iedere aanvragende test een eigen lege lijst ontvangen.

2

Gedeelde voorbereiding gebruiken

Schrijf test_add(cart): voeg via cart_ops.add het item "boek" toe en controleer dat cart daarna precies ["boek"] bevat.

3

Dezelfde fixture opnieuw aanvragen

Schrijf test_take(cart): voeg "kaart" toe, controleer dat cart_ops.take(cart) "kaart" teruggeeft en dat de lijst daarna leeg is.

4

Ook na een fout opruimen

Maak opened_box als yield-fixture: open één box met cart_ops.open_box(), lever haar met yield en sluit haar precies eenmaal in finally.

Onthouden

  • Een parameter vraagt een fixture aan; pytest verzorgt de uitvoering.
  • Maak veranderlijke testgegevens binnen een fixture met passende scope.
  • Lever een resource met yield en ruim haar daarna in finally op.

Hierna controleer je doelbewust welke fouten een functie hoort te geven.

test_cart.py
UitvoerPython
›
›_

Hier komt je resultaat.

Voer je code uit en ontdek
wat je hebt gemaakt.

0/4 stappen
EVEN MEEKIJKEN

Bekijk hoe het kan.

Je mag de oplossing bekijken. Deze poging telt dan niet mee als afgeronde les. Met Reset kun je daarna opnieuw zelf oefenen.