pytest installeren en je eerste test
Waarom handmatig controleren niet meer werkt, waarom een losse assert je bijna niets vertelt, en hoe je pytest installeert en een eerste testbestand schrijft en uitvoert. Inclusief de puntjes, de samenvattingsregel, -q, -v en exitcodes.
Hoe wil je leren?
Het probleem dat we oplossen
Hier is een kleine functie, en de manier waarop de meesten van ons zo'n functie controleren — voer hem uit en kijk naar wat eruit komt:
def discounted(price, percent):
return price - price * percent / 100
print(discounted(200, 10))
print(discounted(80, 25))
print(discounted(99, 0))180.0
60.0
99.0Je leest de drie getallen, rekent het uit in je hoofd en besluit dat ze kloppen. Dat is een test. Het is een handmatige test, en die heeft twee problemen.
Het eerste is dat het niet schaalt. Drie getallen zijn gemakkelijk met het blote oog te controleren. Dertig functies met elk vijf testgevallen, opnieuw gecontroleerd na elke wijziging, zijn dat niet — en niemand doet dat.
Het tweede is erger. Een maand later ruimt iemand de functie wat op:
def discounted(price, percent):
return price * (1 - percent // 100)$ python -c "from prices import discounted; print(discounted(200, 10))"
200Het ziet er redelijk uit, en het draait zonder foutmelding. Maar // is gehele deling, dus 10 // 100 is 0, en elke korting is stilletjes verdwenen. Code die eerst werkte en nu niet meer werkt, heet een regressie, en een regressie is precies wat handmatige controle mist — omdat niemand teruggaat om de oude getallen opnieuw te controleren.
De oplossing is om de verwachte antwoorden in code op te schrijven, zodat een machine ze elke keer kan controleren. Python heeft daar al een statement voor: assert. Zet de checks in een bestand genaamd check_prices.py, naast prices.py:
from prices import discounted
assert discounted(200, 10) == 180.0
assert discounted(80, 25) == 60.0
assert discounted(99, 0) == 99
print("all good")Met de oorspronkelijke functie:
$ python check_prices.py
all goodMet de "opgeruimde" versie:
$ python check_prices.py
Traceback (most recent call last):
File "/home/you/shop/check_prices.py", line 3, in <module>
assert discounted(200, 10) == 180.0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorDe regressie is ontdekt, en dat is vooruitgang. Maar kijk eens wat je te zien krijgt: AssertionError, en verder niets. Niet wat discounted(200, 10) daadwerkelijk opleverde. En het script stopte bij de eerste fout, dus je weet niet of de andere twee checks slagen.
Dit hoofdstuk installeert de tool die beide problemen oplost: pytest.
Aan het einde van dit hoofdstuk kun je
- Uitleggen waarom geautomatiseerde tests regressies ontdekken die handmatige controle mist
- pytest installeren met
uvofpip, en dit bevestigen metpytest --version - Een testbestand schrijven dat pytest vanzelf vindt, en dit uitvoeren
- De puntjes en de samenvattingsregel lezen, en wisselen tussen
-qen-v - Vertellen wat de exitcodes
0en1betekenen, en waarom dat belangrijk is
Voorkennis: voordat je een test schrijft. Je hebt ook Python 3 nodig en je moet weten hoe je een functie schrijft.
Voordat je de test schrijft
Het vorige hoofdstuk ging over beslissen wat je test. Dit hoofdstuk gaat over de concrete zaken die op hun plek moeten staan voordat de eerste regel testcode kan draaien. Het zijn er vier, en als er eentje ontbreekt, levert dat telkens zijn eigen verwarrende foutmelding op — het loont dus om ze op volgorde te controleren.
1. Eén projectmap, één omgeving. Alles in dit hoofdstuk bevindt zich in één enkele map:
shop/
├── .venv/ the project's own Python environment
├── pyproject.toml created by uv; lists pytest as a dev dependency
├── prices.py the code under test
└── test_prices.py the testsWaarom een eigen omgeving? Omdat pytest geïnstalleerd moet worden in dezelfde Python die je code zal importeren. Als pytest in de ene Python staat en de packages van je project in een andere, krijg je foutmeldingen die lijken op ontbrekende code terwijl het echte probleem een ontbrekende tool is. Een .venv per project neemt alle twijfel weg.
2. pytest geïnstalleerd in die omgeving — de volgende paragraaf doet dit.
3. De code moet importeerbaar zijn. Een testbestand is gewone Python: de eerste regel zal from prices import discounted zijn. Controleer dus voordat je ook maar een test schrijft of precies deze import werkt vanuit de projectmap:
$ python -c "from prices import discounted; print(discounted(200, 10))"
180.0Als deze regel faalt, zal pytest op dezelfde manier falen — alleen verpakt in een langer rapport. Dit afzonderlijk controleren kost vijf seconden en scheidt "mijn code kan niet worden gevonden" van "mijn code klopt niet". Het betekent ook dat de modulenaam een geldige Python-naam moet zijn: prices.py kan worden geïmporteerd, my-prices.py en 2prices.py niet.
4. De belofte, opgeschreven als testgevallen. Schrijf, voordat je een test intypt, op wat discounted belooft, één rij per geval:
| Geval | Invoer | Verwacht | | --- | --- | --- | | Typische korting | discounted(200, 10) | 180.0 | | Een tweede, andere korting | discounted(80, 25) | 60.0 | | Randgeval: helemaal geen korting | discounted(99, 0) | 99 |
En net zo bewust: wat er niet op de lijst staat. Een negatief percentage, of eentje boven de 100: de functie belooft daarover op dit moment nog niets, en een test zou alleen maar vastleggen wat de functie toevallig vandaag doet. (Ongeldige invoer weigeren met een error is een eigen belofte — hoofdstuk vier.) De ingebouwde rekenkunde van Python hoort er ook niet op: je test jouw formule, niet of * wel vermenigvuldigt.
Waarom twee gewone gevallen en niet één? Kijk maar eens naar de derde rij vergeleken met de "opgeruimde" functie van het begin van het hoofdstuk: 99 * (1 - 0 // 100) is nog steeds 99. Het nul-procent-geval slaagt zelfs met de bug erin. Een plan dat alleen uit randgevallen bestaat, kan precies de fout missen waar het om gaat, en een plan van slechts één typisch geval mist de randen. Elke rij is er om iets te ontdekken wat de andere niet zouden opmerken.
De rest van het hoofdstuk zet deze tabel om in een testbestand.
pytest installeren
pytest maakt geen deel uit van Python; het is een package dat je in je project installeert. Het is een tool voor het ontwikkelen van de code, niet iets wat het programma nodig heeft wanneer het draait — het wordt dus toegevoegd als een development dependency.
Als je project uv gebruikt, voer je dit uit in de projectmap:
$ uv add --dev pytest
Resolved 7 packages in 59ms
Installed 5 packages in 18ms
+ iniconfig==2.3.1
+ packaging==26.3
+ pluggy==1.6.0
+ pygments==2.21.0
+ pytest==9.1.1uv legt dit vast in pyproject.toml onder een dev-groep, zodat iedereen die het project later opzet dezelfde tool krijgt:
[dependency-groups]
dev = [
"pytest>=9.1.1",
]Als je gewone pip gebruikt binnen een virtuele omgeving, activeer dan de omgeving en voer uit:
$ python -m pip install pytestpip toont de voortgang van het downloaden en eindigt met:
Successfully installed iniconfig-2.3.1 packaging-26.3 pluggy-1.6.0 pygments-2.21.0 pytest-9.1.1python -m pip in plaats van alleen pip zorgt ervoor dat het package terechtkomt in dezelfde Python die je gaat uitvoeren — een veelvoorkomende bron van "ik heb het geïnstalleerd, maar het is er niet".
Controleer nu of het heeft gewerkt:
$ pytest --version
pytest 9.1.1Nog één opmerking voor we verdergaan. Met uv draaien commando's binnen de omgeving van het project wanneer je ze vooraf laat gaan door uv run — dus uv run pytest --version. Met een geactiveerde virtuele omgeving is alleen pytest voldoende. De rest van deze cursus schrijft gewoon pytest; zet er uv run voor als dat is hoe je werkt.
Je eerste testbestand
Twee naamgevingsregels, en meer heeft pytest niet van je nodig:
- de bestandsnaam begint met
test_(of eindigt op_test.py) - elke functie die je wilt uitvoeren begint met
test
Zet de oorspronkelijke, correcte prices.py terug, en maak daarnaast test_prices.py aan:
from prices import discounted
def test_ten_percent_off():
assert discounted(200, 10) == 180.0
def test_quarter_off():
assert discounted(80, 25) == 60.0
def test_no_discount():
assert discounted(99, 0) == 99Elke check uit check_prices.py is nu zijn eigen kleine functie, en elke naam vertelt wat er wordt gecontroleerd. Er is geen print, geen lijst met tests om te registreren, geen if __name__ == "__main__". Voer nu pytest uit vanuit de projectmap:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 3 items
test_prices.py ... [100%]
============================== 3 passed in 0.01s ===============================pytest doorzocht de map, vond test_prices.py dankzij de naam, vond de drie functies dankzij hún namen, voerde ze allemaal uit en bracht verslag uit.
De uitvoer lezen
Bekijk het regel voor regel.
platform ...— welke Python en welke pytest zijn uitgevoerd. Een blik waard wanneer resultaten verschillen tussen twee computers.rootdir: /home/you/shop— de map die pytest beschouwt als de hoofdmap van het project.collected 3 items— hoeveel tests er zijn gevonden. Het vinden van tests heet collection. Als dit getal lager is dan je verwacht, klopt er ergens een naam niet.test_prices.py ...— één teken per test, in de volgorde waarin ze zijn uitgevoerd. Een.betekent geslaagd.[100%]— voortgang door de hele testrun.3 passed in 0.01s— de samenvattingsregel. Dit is de regel die je als eerste leest.
Zet nu de "opgeruimde" discounted weer terug en voer pytest opnieuw uit. Dit keer met -q, waar we zo dadelijk uitgebreider naar kijken:
$ pytest -q
FF. [100%]
=================================== FAILURES ===================================
_____________________________ test_ten_percent_off _____________________________
def test_ten_percent_off():
> assert discounted(200, 10) == 180.0
E assert 200 == 180.0
E + where 200 = discounted(200, 10)
test_prices.py:5: AssertionError
_______________________________ test_quarter_off _______________________________
def test_quarter_off():
> assert discounted(80, 25) == 60.0
E assert 80 == 60.0
E + where 80 = discounted(80, 25)
test_prices.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_prices.py::test_ten_percent_off - assert 200 == 180.0
FAILED test_prices.py::test_quarter_off - assert 80 == 60.0
2 failed, 1 passed in 0.01sDe voortgangsregel is nu FF.: een F voor elke mislukking. En vergelijk dit eens met de kale AssertionError van daarnet:
- elke test is uitgevoerd — een fout in de ene hield de andere niet tegen, zodat je kunt zien dat nul procent nog steeds werkt
- elke mislukking noemt de test en de regel
assert 200 == 180.0toont de waarde die de functie daadwerkelijk retourneerde
Het volledig lezen van dit rapport is het onderwerp van het hele volgende hoofdstuk. Merk voor nu alleen op dat de samenvattingsregel veranderde in 2 failed, 1 passed — en dat dezelfde assert die je al kende nu zichzelf uitlegt.
-q en -v: minder en meer
De standaarduitvoer is een middenweg. Twee vlaggen sturen die de ene of de andere kant op.
-q (quiet) laat de header weg:
$ pytest -q
... [100%]
3 passed in 0.01s-v (verbose) geeft elke test een eigen regel, met zijn volledige naam:
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/shop
collecting ... collected 3 items
test_prices.py::test_ten_percent_off PASSED [ 33%]
test_prices.py::test_quarter_off PASSED [ 66%]
test_prices.py::test_no_discount PASSED [100%]
============================== 3 passed in 0.01s ===============================test_prices.py::test_ten_percent_off is de node-ID van de test: het bestand, twee dubbele punten, de functie. Je zult dit later gebruiken om een afzonderlijke test uit te voeren. Dit is ook waar beschrijvende testnamen hun waarde bewijzen — in een overzicht met -v vertelt test_quarter_off je wat er geslaagd is; test_2 zou dat niet doen.
Gebruik -q wanneer je voortdurend tests uitvoert en alleen het eindoordeel wilt zien, en -v wanneer je precies wilt zien welke tests er hebben gedraaid.
Wat is .pytest_cache? Een map die pytest aanmaakt om dingen tussen testruns te onthouden, zoals welke tests de vorige keer faalden. Je kunt hem veilig verwijderen en hij hoort niet thuis in versiebeheer.Exitcodes: hoe een machine het resultaat leest
Jij leest de samenvattingsregel. Een script, een editor of een buildserver kan dat niet — die leest de exitcode, het getal dat elk programma teruggeeft aan de shell wanneer het klaar is. In een Unix-shell bevat $? de laatste exitcode:
$ pytest -q
... [100%]
3 passed in 0.01s
$ echo $?
0En met de kapotte functie, na de run die eindigde in 2 failed, 1 passed:
$ echo $?
10— alles gevonden, alles geslaagd1— tests zijn uitgevoerd, en minstens één is mislukt
(In PowerShell heet deze variabele $LASTEXITCODE.) Dit is wat ervoor zorgt dat tests automatisch kunnen draaien voordat code wordt gemerged: een exitcode ongelijk aan nul stopt het proces. Er zijn nog een paar andere codes, waarvan je er eentje hieronder tegenkomt in "Wanneer het misgaat".
Waar testbestanden staan
In dit hoofdstuk staat het testbestand vlak naast de code:
shop/
├── prices.py
└── test_prices.pyVoor een handvol bestanden is dat prima. De meeste projecten verzamelen tests in plaats daarvan in een eigen map tests/. In beide gevallen vindt pytest ze op naam; hoe je een echt project indeelt en wat er verandert als je dat doet, is onderwerp van hoofdstuk drie.
Een compleet voorbeeld
Een functie die een score omzet in een lettercijfer, grades.py:
def letter_grade(score):
"""Turn a score from 0 to 100 into a letter."""
if score >= 80:
return "A"
if score >= 60:
return "B"
if score >= 40:
return "C"
return "F"En de tests daarvan, test_grades.py:
from grades import letter_grade
def test_top_score_is_an_a():
assert letter_grade(95) == "A"
def test_exactly_eighty_is_still_an_a():
assert letter_grade(80) == "A"
def test_just_below_eighty_is_a_b():
assert letter_grade(79) == "B"
def test_middle_score_is_a_c():
assert letter_grade(50) == "C"
def test_zero_is_an_f():
assert letter_grade(0) == "F"$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/grades
collecting ... collected 5 items
test_grades.py::test_top_score_is_an_a PASSED [ 20%]
test_grades.py::test_exactly_eighty_is_still_an_a PASSED [ 40%]
test_grades.py::test_just_below_eighty_is_a_b PASSED [ 60%]
test_grades.py::test_middle_score_is_a_c PASSED [ 80%]
test_grades.py::test_zero_is_an_f PASSED [100%]
============================== 5 passed in 0.01s ===============================Twee dingen zijn het opmerken waard.
Ten eerste: de tests bij 80 en 79. Deze liggen aan weerszijden van een grens, en dat is waar fouten zich schuilhouden. Verander score >= 80 in score > 80 — een vergissing die snel gemaakt is — en voer de stille variant uit:
$ pytest -q
.F... [100%]
=================================== FAILURES ===================================
______________________ test_exactly_eighty_is_still_an_a _______________________
def test_exactly_eighty_is_still_an_a():
> assert letter_grade(80) == "A"
E AssertionError: assert 'B' == 'A'
E
E - A
E + B
test_grades.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_grades.py::test_exactly_eighty_is_still_an_a - AssertionError: as...
1 failed, 4 passed in 0.01sDe tweede test, test_exactly_eighty_is_still_an_a, is degene die faalt: 80 krijgt nu een 'B'. Een test bij alleen 95 had dit nooit opgemerkt.
Ten tweede: de namen. In het -v-overzicht leest elke regel als een zin over het gedrag van de functie. Als er over een paar maanden eentje faalt, vertelt alleen de naam je al welke belofte is geschonden.
Wanneer het misgaat
zsh: command not found: pytest (of bash: pytest: command not found) De shell kan het programma pytest niet vinden. Het is niet geïnstalleerd, of het is geïnstalleerd in een virtuele omgeving die niet actief is. Voer met uv uv run pytest uit; activeer met pip eerst de omgeving. python -m pytest werkt ook, en voert pytest uit met de Python waar python naar verwijst.
/usr/bin/python3: No module named pytest Je voerde python -m pytest uit, en die specifieke Python heeft geen pytest. Het pad aan het begin vertelt je welke Python het was — hier de Python van het systeem, niet die van je project. Installeer pytest in de omgeving die je bedoelt te gebruiken.
ModuleNotFoundError: No module named 'price' tijdens de collection Het testbestand kon je code niet importeren — hier door een typfout: from price import ... in plaats van from prices import ...:
$ pytest -q
==================================== ERRORS ====================================
_______________________ ERROR collecting test_prices.py ________________________
ImportError while importing test module '/home/you/shop/test_prices.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_prices.py:1: in <module>
from price import discounted
E ModuleNotFoundError: No module named 'price'
=========================== short test summary info ============================
ERROR test_prices.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sLet op het woord ERROR, niet FAILED: er is helemaal geen test uitgevoerd omdat het bestand niet eens kon worden geladen, en de exitcode is 2. Lees de laatste regel met E, en voer vervolgens de import-check uit "Voordat je de test schrijft" uit — python -c "from prices import discounted" — totdat die zelfstandig werkt.
no tests ran in 0.01s pytest is uitgevoerd, maar heeft niets gevonden. De bestandsnaam begint niet met test_ (tests_prices.py, prices_tests.py en test.py worden allemaal gemist), of de functies beginnen niet met test (check_quarter_off wordt stilletjes genegeerd). De exitcode is hier 5, niet 0, zodat een geautomatiseerde run "niets gevonden" niet aanziet voor "alles geslaagd".
Een test slaagt terwijl dat overduidelijk niet zou moeten Zoek naar een ontbrekende assert:
from prices import discounted
def test_quarter_off():
discounted(80, 25) == 999$ pytest -q
. [100%]
1 passed in 0.01sDe vergelijking wordt berekend en weggegooid. Een test faalt alleen wanneer er binnenin iets een exception veroorzaakt; zonder assert gebeurt dat niet.
PytestReturnNotNoneWarning: Test functions should return None Je hebt return discounted(99, 0) == 99 geschreven in plaats van assert .... pytest negeert wat een test retourneert, dus deze test slaagt ongeacht of de vergelijking waar of onwaar is. De waarschuwing vraagt zelfs: Did you mean to use assert instead of return?
Stap 4 van 6 — Voorspellen
Test je begrip
Dit bestand is opgeslagen als test_double.py en pytest -q wordt uitgevoerd. Welke tekens staan aan het begin van de eerste regel van de uitvoer?
def double(n):
return n * 2
def test_two():
assert double(2) == 4
def test_zero():
assert double(0) == 1
def test_negative():
assert double(-3) == -6- AF
- B..F
- C.F.
- DF..
999 is overduidelijk het verkeerde antwoord, en toch meldt pytest 1 passed. Waarom?
def discounted(price, percent):
return price - price * percent / 100
def test_quarter_off():
discounted(80, 25) == 999- Apytest controleert alleen of de functie is uitgevoerd, niet wat het resultaat is
- BDe regel heeft geen `assert` — de vergelijking wordt berekend en weggegooid
- CDecimale getallen kunnen niet worden vergeleken met `==`
- DDe functienaam begint niet met `test_`
Een bestand met correcte tests erin heet double_checks.py. Wat gebeurt er als je pytest uitvoert in die map?
- Apytest stopt met een `ImportError`
- B`no tests ran` — de bestandsnaam begint niet met `test_`, dus er wordt nooit in gezocht; exitcode `5`
- CElke test wordt uitgevoerd, omdat de functienamen beginnen met `test_`
- Dpytest hernoemt het bestand voor je naar `test_double_checks.py`
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Maak een map met een bestand temperature.py met daarin twee functies:
c_to_f(celsius)— retourneertcelsius * 9 / 5 + 32f_to_c(fahrenheit)— retourneert(fahrenheit - 32) * 5 / 9
Installeer vervolgens pytest in de omgeving van die map en bevestig dit met pytest --version.
Schrijf test_temperature.py met minstens vijf tests. Neem het vriespunt van water op (0 en 32), het kookpunt van water (100 en 212), en -40, wat in beide schalen gelijk is. Geef elke test een naam die vertelt wat hij controleert.
Voer het op drie manieren uit — pytest, pytest -q en pytest -v — en controleer na een daarvan de exitcode.
Maak het daarna met opzet stuk: verander 9 / 5 in 5 / 9 in c_to_f. Voorspel voordat je het uitvoert welke tests zullen falen en hoe de voortgangsregel eruit zal zien. Voer pytest -q uit, vergelijk het, en controleer de exitcode opnieuw.
Hernoem tot slot test_temperature.py naar temperature_check.py en voer pytest uit. Lees wat er staat en bekijk de exitcode. Geef het daarna zijn oorspronkelijke naam weer terug. Weten hoe "niets gevonden" eruitziet bespaart je een verwarrende middag.
Oplossing
Eerst het plan, vóór enige code — dezelfde tabel als eerder:
| Geval | Invoer | Verwacht | | --- | --- | --- | | Water bevriest | c_to_f(0), f_to_c(32) | 32, 0 | | Water kookt | c_to_f(100), f_to_c(212) | 212, 100 | | De twee schalen komen samen | c_to_f(-40), f_to_c(-40) | -40, -40 |
temperature.py:
def c_to_f(celsius):
return celsius * 9 / 5 + 32
def f_to_c(fahrenheit):
return (fahrenheit - 32) * 5 / 9Vóór het schrijven van tests, de import-check vanuit de projectmap:
$ python -c "import temperature; print('importable')"
importabletest_temperature.py:
from temperature import c_to_f, f_to_c
def test_water_freezes_at_32_f():
assert c_to_f(0) == 32
def test_water_boils_at_212_f():
assert c_to_f(100) == 212
def test_32_f_is_zero_c():
assert f_to_c(32) == 0
def test_212_f_is_100_c():
assert f_to_c(212) == 100
def test_minus_40_is_the_same_in_both_scales():
assert c_to_f(-40) == -40
assert f_to_c(-40) == -40$ pytest -q
..... [100%]
5 passed in 0.01s
$ echo $?
0
$ pytest -v
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/you/temperature
collecting ... collected 5 items
test_temperature.py::test_water_freezes_at_32_f PASSED [ 20%]
test_temperature.py::test_water_boils_at_212_f PASSED [ 40%]
test_temperature.py::test_32_f_is_zero_c PASSED [ 60%]
test_temperature.py::test_212_f_is_100_c PASSED [ 80%]
test_temperature.py::test_minus_40_is_the_same_in_both_scales PASSED [100%]
============================== 5 passed in 0.01s ===============================Nu de opzettelijke bug, 9 / 5 veranderd in 5 / 9 in c_to_f:
$ pytest -q
.F..F [100%]
=================================== FAILURES ===================================
__________________________ test_water_boils_at_212_f ___________________________
def test_water_boils_at_212_f():
> assert c_to_f(100) == 212
E assert 87.55555555555556 == 212
E + where 87.55555555555556 = c_to_f(100)
test_temperature.py:9: AssertionError
___________________ test_minus_40_is_the_same_in_both_scales ___________________
def test_minus_40_is_the_same_in_both_scales():
> assert c_to_f(-40) == -40
E assert 9.777777777777779 == -40
E + where 9.777777777777779 = c_to_f(-40)
test_temperature.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_temperature.py::test_water_boils_at_212_f - assert 87.55555555555...
FAILED test_temperature.py::test_minus_40_is_the_same_in_both_scales - assert...
2 failed, 3 passed in 0.01s
$ echo $?
1En het hernoemde bestand:
$ pytest -q
no tests ran in 0.01s
$ echo $?
5Waarom het op deze manier is geschreven:
- Eén gedrag per test, met een naam als een zin. In het
-v-overzicht vertelt elke regel wat er is beloofd. Wanneertest_water_boils_at_212_ffaalt, weet je al wat er stuk is voordat je verder leest. - Bekende, controleerbare waarden.
0/32,100/212en-40zijn feiten die je zonder rekenmachine kunt verifiëren, waardoor een mislukking naar de code wijst en niet naar een twijfelachtige verwachte waarde. - Beide richtingen.
c_to_fenf_to_czijn afzonderlijke code, dus elk heeft eigen tests nodig; dat de ene klopt zegt niets over de andere. - De run met de bug is het meest veelzeggende onderdeel. Slechts twee tests faalden, en
test_water_freezes_at_32_fwas daar niet bij:0 * 5 / 9 + 32is nog steeds32, omdat vermenigvuldigen met nul elke vermenigvuldigingsfactor verbergt. Dat is precies waarom het plan meer dan één testgeval heeft — een testsuite met alleen het vriespunt zou met de bug erin geslaagd zijn. De test voor-40heeft tweeassert-regels; de eerste faalde, waardoor de tweede nooit is uitgevoerd. Dat is hier acceptabel omdat beide dezelfde belofte beschrijven. - De exitcodes —
0,1,5— zijn de drie die je het vaakst tegenkomt: alles geslaagd, er is iets mislukt, niets gevonden.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken