Een verse lijst per test
Maak de fixture cart met @pytest.fixture. Laat iedere aanvragende test een eigen lege lijst ontvangen.
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.
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.
import pytest
@pytest.fixture
def notes():
return []
def test_add(notes):
notes.append("idee")
assert notes == ["idee"]
def test_empty(notes):
assert notes == []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))gevonden: 2 geslaagd: 2 mislukt: 0 status: 0
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.
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.
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"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))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.
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.
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.
Maak de fixture cart met @pytest.fixture. Laat iedere aanvragende test een eigen lege lijst ontvangen.
Schrijf test_add(cart): voeg via cart_ops.add het item "boek" toe en controleer dat cart daarna precies ["boek"] bevat.
Schrijf test_take(cart): voeg "kaart" toe, controleer dat cart_ops.take(cart) "kaart" teruggeeft en dat de lijst daarna leeg is.
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.
Hierna controleer je doelbewust welke fouten een functie hoort te geven.
Voer je code uit en ontdek
wat je hebt gemaakt.
Je hoeft het niet in één keer goed te doen. Na het uitvoeren zie je hier welke stappen al kloppen.