De klok als afhankelijkheid ontvangen
Vul expired(deadline, *, clock) in. Roep de meegegeven clock precies eenmaal aan en geef een bool terug: vanaf de deadline is de uitkomst True.
Een test die op de echte klok wacht, kan traag worden of precies rond een tijdgrens wisselend slagen. Een test die echt een bericht verstuurt, verandert bovendien de buitenwereld. Je maakt zulke code beter testbaar door de benodigde klok en schrijver expliciet te ontvangen.
Een afhankelijkheid is iets wat je functie nodig heeft om haar werk te doen. Een klok levert een tijdwaarde; een schrijver ontvangt uitvoer. Beide kunnen gewone functies zijn die je als argument doorgeeft.
Bij clock=clock geef je de functie zelf door. De ontvangende code kiest wanneer zij clock() aanroept. Dat verschilt van clock=clock(), waarmee je meteen de tijdwaarde doorgeeft.
Het eerste voorbeeld gebruikt een vaste klok. De tests kunnen daardoor verschillende tijden onderzoeken zonder te slapen. Dezelfde berekening draait iedere keer met dezelfde gekozen invoer.
import pytest
def elapsed(start, *, clock):
return clock() - start
@pytest.mark.parametrize("now,expected", [(20, 5), (25, 10)])
def test_elapsed(now, expected):
assert elapsed(15, clock=lambda: now) == expectedimport 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_elapsed.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
Een deterministische test geeft bij dezelfde gecontroleerde omstandigheden hetzelfde resultaat. Je kiest bijvoorbeeld een tijd net vóór, op en na een deadline. De test hoeft de echte deadline niet af te wachten.
In deze oefening zijn tijden eenvoudige gehele getallen in dezelfde zelfgekozen eenheid. De klok geeft zo'n getal terug. Echte datumformaten en tijdzones voegen andere afspraken toe; voor de huidige vergelijking is een vaste waarde voldoende.
Een testbaar ontwerp vraagt ook hoeveel keer de klok wordt gelezen. Twee klokaanroepen kunnen verschillende waarden opleveren. Hier lees je eenmaal, bewaart die waarde en gebruikt haar voor de hele boodschap.
Een testdubbel neemt in een test de plaats in van een afhankelijkheid. Een registrerende schrijver kan zijn ontvangen tekst in een lokale lijst zetten. Daarna controleert de test wat de echte functie aan die schrijver heeft doorgegeven.
Het tweede voorbeeld controleert zowel de ontvangen tekst als de retourwaarde van de schrijver. Dezelfde functie wordt ook met een falende schrijver uitgevoerd. Je ziet daarmee meer dan alleen de uiteindelijke printuitvoer.
import pytest
def send_name(name, *, writer):
return writer("Hoi " + name)
def test_written():
sent = []
def writer(message):
sent.append(message)
return len(message)
assert send_name("Mila", writer=writer) == 8
assert sent == ["Hoi Mila"]
def test_writer_error():
problem = OSError("schrijven mislukt")
def writer(message):
raise problem
with pytest.raises(OSError) as error:
send_name("Mila", writer=writer)
assert error.value is problemimport 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_send.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
Deze schrijver verstuurt geen echt bericht: hij maakt het uitvoercontract zichtbaar. Een aparte integratietest kan later een echte bestandsschrijver onderzoeken. Het voordeel van de kleine unit is dat je nu precies kunt aanwijzen welke argumenten en fouten worden doorgegeven.
Een onverwachte schrijffout is geen succesvolle verzending. Laat haar doorlopen wanneer de aanroeper haar moet afhandelen. except Exception: return None verbergt de oorzaak en maakt een mislukking op een gewone retourwaarde lijken.
Een nieuw foutobject met dezelfde melding maken bewaart ook niet alles. De oorspronkelijke fout heeft een eigen identiteit en traceback. Zonder onnodige handler blijven die vanzelf behouden.
Vul expired en notify in het bestand reminders.py in. Bij het bereiken van de deadline is een herinnering verlopen. Notify schrijft één boodschap in de vorm tijd: tekst en geeft de schrijversretourwaarde ongewijzigd terug.
Een schrijversretour kan ook nul, False of een lege string zijn. Geef die waarde gewoon terug; een constructie zoals receipt or None verandert haar betekenis. Geeft de schrijver een object terug, dan behoud je hetzelfde object.
Open daarna test_reminders.py voor je eigen pytest-test. Gebruik een klokwaarde tweeënveertig, een lijst voor ontvangen tekst en een schrijver die negentien teruggeeft. Controleer die waarden onafhankelijk, zodat een verkeerde tijd, tekst of retourwaarde zichtbaar wordt.
Vul expired(deadline, *, clock) in. Roep de meegegeven clock precies eenmaal aan en geef een bool terug: vanaf de deadline is de uitkomst True.
Vul notify(text, *, clock, writer) in. Lees clock eenmaal, geef writer precies "{tijd}: {tekst}" en geef de retourwaarde van writer ongewijzigd terug.
Open test_reminders.py. Laat test_notify de tekst "start" met een klokwaarde 42 schrijven. Controleer de lijst ["42: start"], de schrijversretourwaarde 19 en precies één klokaanroep.
Zorg dat notify een fout van clock of writer laat doorlopen. Vang haar niet om een succeswaarde terug te geven en maak geen vervangend foutobject.
In de volgende module verbind je deze afspraken aan echte bestanden en verdere invoer en uitvoer.
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.