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

De juiste fout verwachten en andere fouten laten opvallen

Na deze les kun je
  • Met pytest.raises een afgesproken fout verwachten.
  • Een volledige melding en zo nodig een exacte foutklasse controleren.
  • Naast foutpaden ook geldige invoer blijven testen.

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.

Een fout als verwachte uitkomst

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.

Een specifieke fout én normale route controleren
test_read.py
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") == 8
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_read.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

Een melding precies controleren

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.

Een fouttype omvat gewoonlijk zijn subklassen

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.

Veelgemaakte fout: iedere fout is goed

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.

Een brede foutverwachting laat de verkeerde fout door
test_bug.py
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")
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_bug.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: 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.

Het contract van de aantallezer

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.

AAN DE SLAG 0/4
1

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.

2

Een ander fouttype onderscheiden

Schrijf test_wrong_type: counts.read(7) moet exact TypeError met melding "tekst vereist" geven.

3

De volledige melding en klasse controleren

Schrijf test_invalid_text: counts.read("12x") moet exact ValueError met melding "ongeldig aantal" geven, zonder extra tekst of subklasse.

4

De normale route behouden

Vul test_valid(text, expected) in: controleer voor de twee gegeven rijen de echte retourwaarde met assert.

Onthouden

  • Zet de echte aanroep binnen pytest.raises en kies het afgesproken fouttype.
  • Vergelijk str(error.value) rechtstreeks als de volledige melding exact moet kloppen.
  • Controleer een exacte klasse alleen als het contract dat verlangt, en behoud tests voor geldige invoer.

Hierna kies je testgegevens rond grenzen, zodat een kleine verschuiving niet ongemerkt blijft.

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