Een ontbrekende fout vinden
Schrijf test_empty: counts.read("") moet exact ValueError met melding "lege invoer" geven. Laat ook een uitvoering zonder fout je test doen mislukken.
Een ongeldige aanvraag hoort soms te mislukken. Een test die alleen op een getal wacht, beschrijft dat contract onvolledig. Je wilt vastleggen welke fout de aanroeper kan verwachten, en tegelijk voorkomen dat een onverwachte programmeerfout als succes telt.
Met pytest.raises beschrijf je dat één blok een bepaald fouttype moet geven. Zet de te onderzoeken aanroep binnen het with-blok. Blijft de fout uit, dan mislukt de test.
Het blok vangt een passende fout op, waarna de test verder kan. Een fout van een ander type blijft een mislukking. Dat verschilt van een algemene try/except Exception die ieder probleem hetzelfde kan behandelen.
Het eerste voorbeeld test een kleine cijferlezer. De ene test onderzoekt ongeldige tekst, de andere een gewone waarde. Beide routes horen bij de afspraak van de functie.
import pytest
def read_digit(text):
if len(text) != 1 or text not in "0123456789":
raise ValueError("één cijfer vereist")
return int(text)
def test_invalid():
with pytest.raises(ValueError) as error:
read_digit("xx")
assert type(error.value) is ValueError
assert str(error.value) == "één cijfer vereist"
def test_normal():
assert read_digit("8") == 8import 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_read.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
Met as error bewaar je informatie over de opgevangen fout. De oorspronkelijke foutwaarde staat na het blok in error.value. Met str(error.value) maak je haar melding tot een gewone string die je kunt vergelijken.
Voor een exacte melding schrijf je bijvoorbeeld assert str(error.value) == "lege invoer". Die vergelijking weigert ook extra spaties of een afsluitende newline. Je controleert daarmee precies de afgesproken tekst.
Het argument match van pytest.raises gebruikt juist een reguliere expressie. Dat is handig voor een variabel deel van een melding. De ankers ^ en $ zijn geen vervanging voor letterlijke gelijkheid: $ kan ook vóór een afsluitende newline passen.
Kies de vorm die bij het contract hoort. In deze oefening zijn de meldingen vaste strings, dus vergelijken we ze rechtstreeks. Je hoeft daarvoor geen nieuw regexpatroon te ontwerpen.
Pytest accepteert bij raises(ValueError) ook een subklasse van ValueError. Vaak is dat precies de bedoelde afspraak: de aanroeper kan die hele familie opvangen. Een exacte klasse eisen is dus een bewuste extra keuze.
Het contract van deze oefening noemt nadrukkelijk de gewone ValueError en TypeError. Controleer daarom na het blok ook type(error.value) is ValueError of de overeenkomstige TypeError. Zo leer je het verschil tussen een foutfamilie en een exacte klasse.
Het tweede voorbeeld bevat een implementatiefout die KeyError geeft. De brede verwachting Exception wordt groen, terwijl de specifieke verwachting ValueError mislukt. De groene eerste test beschrijft dus een te ruime afspraak.
import pytest
def read_digit(text):
raise KeyError("onverwachte implementatiefout")
def test_too_broad():
with pytest.raises(Exception):
read_digit("xx")
def test_specific():
with pytest.raises(ValueError):
read_digit("xx")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_bug.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: 1 mislukt: 1 status: 1
Kies een fouttype vanuit het contract, niet vanuit de fout die toevallig verschijnt. Een onverwachte KeyError wil je onderzoeken. Verander de test niet zomaar om iedere mislukking te accepteren.
Roep de functie ook echt aan. Zelf binnen het blok raise ValueError(...) schrijven bewijst alleen dat Python die fout kan maken. De les controleert daarom dat de aangewezen bibliotheek de invoer daadwerkelijk ontving.
De vaste module counts verwacht tekst met uitsluitend ASCII-cijfers en geeft daarvoor een geheel getal terug. Lege tekst geeft exact ValueError met lege invoer. Een ander argumenttype geeft exact TypeError met tekst vereist; andere tekst geeft exact ValueError met ongeldig aantal.
Schrijf voor deze drie fouten een afzonderlijke test en behoud de gegeven normale invoerrijen. Je controles moeten twee correcte uitvoeringen accepteren. Ze moeten ook ontbreken van een fout, een verkeerd type, extra meldingstekst en een verkeerde normale uitkomst laten opvallen.
Schrijf test_empty: counts.read("") moet exact ValueError met melding "lege invoer" geven. Laat ook een uitvoering zonder fout je test doen mislukken.
Schrijf test_wrong_type: counts.read(7) moet exact TypeError met melding "tekst vereist" geven.
Schrijf test_invalid_text: counts.read("12x") moet exact ValueError met melding "ongeldig aantal" geven, zonder extra tekst of subklasse.
Vul test_valid(text, expected) in: controleer voor de twee gegeven rijen de echte retourwaarde met assert.
Hierna kies je testgegevens rond grenzen, zodat een kleine verschuiving niet ongemerkt blijft.
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.