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

Tijd en uitvoer voorspelbaar testen met afhankelijkheden

Na deze les kun je
  • Een klok en schrijver als functieargument doorgeven.
  • Met een vaste klok en registrerende schrijver echte waarden controleren.
  • Retours en onverwachte fouten van afhankelijkheden behouden.

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 functie kan ook invoer zijn

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.

Tijdgedrag controleren zonder te wachten
test_elapsed.py
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) == expected
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_elapsed.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 grens testen zonder de tijd te laten verstrijken

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.

Uitvoer waarnemen met een eigen schrijver

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.

Echte argumenten en fouten van een eigen schrijver controleren
test_send.py
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 problem
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_send.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

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.

Veelgemaakte fout: dezelfde fout opnieuw maken

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.

Een herinnering opbouwen en zelf testen

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.

AAN DE SLAG 0/4
1

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.

2

De geschreven boodschap en retourwaarde behouden

Vul notify(text, *, clock, writer) in. Lees clock eenmaal, geef writer precies "{tijd}: {tekst}" en geef de retourwaarde van writer ongewijzigd terug.

3

Een eigen test met gecontroleerde uitvoer schrijven

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.

4

Onverwachte fouten ongewijzigd doorgeven

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.

Onthouden

  • Geef klok en schrijver als functies door en kies hun gedrag in de test.
  • Controleer echte ontvangen argumenten, aantallen aanroepen en retourwaarden.
  • Laat onverwachte fouten hun oorspronkelijke betekenis en identiteit behouden.

In de volgende module verbind je deze afspraken aan echte bestanden en verdere invoer en uitvoer.

reminders.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.