Vertegenwoordigers uit iedere groep
Maak test_groups met aparte parametrisatiegevallen voor 500→4, 1500→7 en 2500→9. Controleer telkens freight.fee(weight).
Een functie kan voor honderd gewone invoeren goed lijken en toch precies op een overgang fout zijn. Bij een verzendtarief is bijvoorbeeld belangrijk of duizend gram nog bij het eerste tarief hoort. Je kiest testgegevens daarom vanuit de afspraken, niet alleen vanuit makkelijke voorbeeldgetallen.
Een partitie is hier een groep invoeren waarvoor dezelfde gedragsregel geldt. Als een tarief tot en met duizend gram vier is, vormen die geldige gewichten samen één groep. Een zwaardere groep kan een ander tarief krijgen.
Kies eerst een duidelijke vertegenwoordiger uit iedere groep. Daarmee controleer je dat de hoofdregels bestaan. Eén voorbeeld per groep bewijst niet dat alle waarden goed zijn, maar helpt wel om een vergeten tak te ontdekken.
Neem de verwachting uit de beschreven afspraak. Dezelfde functie voor de ontvangen én verwachte waarde gebruiken laat haar fouten aan beide kanten terugkomen. Een test moet juist een onafhankelijk vergelijkingspunt hebben.
Een grensgeval ligt op of dicht bij de overgang tussen twee regels. Bij gehele getallen kun je het getal net vóór de grens, de grens zelf en het getal erna kiezen. Dat maakt het verschil tussen < en <= zichtbaar.
Het eerste voorbeeld gebruikt een fictief toegangskaartje. Vanaf twaalf kost het kaartje acht; daaronder nul. De rijen elf, twaalf en dertien leggen precies vast waar die overgang zit.
import pytest
def ticket(age):
return 0 if age < 12 else 8
@pytest.mark.parametrize("age,expected", [(11, 0), (12, 8), (13, 8)])
def test_ticket(age, expected):
assert ticket(age) == 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_ticket.py"], plugins=[recorded])
print("gevonden:", recorded.count)
print("geslaagd:", recorded.outcomes.count("passed"))
print("mislukt:", recorded.outcomes.count("failed"))
print("status:", int(status))gevonden: 3 geslaagd: 3 mislukt: 0 status: 0
Het tweede voorbeeld verschuift die grens per ongeluk. De gewone leeftijden vijf en achttien blijven goed; de rij twaalf faalt. Meer willekeurige gewone waarden hadden deze specifieke fout niet noodzakelijk gevonden.
import pytest
def ticket(age):
return 0 if age <= 12 else 8
@pytest.mark.parametrize("age,expected", [(5, 0), (18, 8), (12, 8)])
def test_ticket(age, expected):
assert ticket(age) == 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_shift.py"], plugins=[recorded])
print("gevonden:", recorded.count)
print("geslaagd:", recorded.outcomes.count("passed"))
print("mislukt:", recorded.outcomes.count("failed"))
print("status:", int(status))gevonden: 3 geslaagd: 2 mislukt: 1 status: 1
Een geldig bereik heeft soms een minimum en maximum. Test die uitersten als geldige invoer, en kies daarnaast waarden net daarbuiten die moeten worden afgewezen. Zo controleer je zowel accepteren als weigeren.
Een afgewezen waarde mag niet ongemerkt een gewoon resultaat krijgen. Gebruik daarvoor de foutverwachting uit de vorige les. Een verwachte fout en een gewone retourwaarde zijn verschillende afspraken.
Stel dat de beschrijving zegt tot en met duizend, maar de code gebruikt weight < 1000. Dan hoort de test met duizend te mislukken. De verwachting veranderen naar het hogere tarief zou de implementatiefout juist vastleggen als nieuwe regel.
Reken daarom eerst het contract na. Kijk vervolgens welke vergelijking of tak het gedrag veroorzaakt. Een mislukte grensrij geeft je een kleine plek om te onderzoeken.
De module freight gebruikt gehele grammen van één tot en met drieduizend. Het tarief is vier tot en met duizend, zeven van duizend-één tot en met tweeduizend en negen daarboven tot en met drieduizend. Buiten het bereik geeft de functie ValueError met gewicht buiten bereik.
Schrijf geparametriseerde tests voor vertegenwoordigers, beide interne overgangen en de buitenranden. Gebruik per gevraagde waarde een afzonderlijk pytest-geval. Daarmee blijft zichtbaar welk onderdeel van de afspraak faalt.
De les probeert ook correcte alternatieve uitvoeringen. Je test moet hun afgesproken resultaten accepteren en gerichte verschuivingen vinden. Daarmee oefen je het contract, zonder één specifieke schrijfwijze van de productcode voor te schrijven.
Maak test_groups met aparte parametrisatiegevallen voor 500→4, 1500→7 en 2500→9. Controleer telkens freight.fee(weight).
Maak test_first_boundary met aparte gevallen 999→4, 1000→4 en 1001→7. Laat een onterechte < bij 1000 opvallen.
Maak test_second_boundary met aparte gevallen 1999→7, 2000→7 en 2001→9.
Maak test_limits met aparte gevallen 1→4 en 3000→9. Maak test_outside met aparte gewichten 0 en 3001: verwacht ValueError met "gewicht buiten bereik".
Hierna maak je tests voorspelbaar wanneer code van tijd of uitvoer afhangt.
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.