assert en foutrapporten lezen
Hoe pytest binnenin een gewone assert kijkt, hoe je een foutrapport regel voor regel leest, en welke vergelijking je schrijft zodat het rapport je het meeste vertelt.
Hoe wil je leren?
Het probleem dat we oplossen
Hier is een check geschreven in gewone Python, in een bestand genaamd check.py:
def full_name(first, last):
return f"{first} {last}".title()
assert full_name("ada", "lovelace") == "Ada Lovelace "Traceback (most recent call last):
File "/home/you/shop/check.py", line 5, in <module>
assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorPython vertelt je dat de bewering onwaar was, en verder niets. Wat leverde full_name daadwerkelijk op? Je zou een print moeten toevoegen, het opnieuw moeten uitvoeren en moeten kijken.
Zet dezelfde regel in een test, test_names.py, en voer pytest -q uit:
from names import full_name
def test_full_name():
assert full_name("ada", "lovelace") == "Ada Lovelace "F [100%]
=================================== FAILURES ===================================
________________________________ test_full_name ________________________________
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
E AssertionError: assert 'Ada Lovelace' == 'Ada Lovelace '
E
E - Ada Lovelace
E ? -
E + Ada Lovelace
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionError: assert 'Ada Lovelace' ...
1 failed in 0.01sNu kun je beide waarden zien, plus een markering onder het ene teken dat verschilt: een spatie aan het einde van de verwachte string. De functie klopte; de test zat ernaast.
Een falende test is maar zo nuttig als wat hij je vertelt. Dit hoofdstuk gaat over het lezen van alles wat pytest je vertelt, en over het schrijven van asserts die het iets geven wat de moeite waard is om te zeggen.
Aan het einde van dit hoofdstuk kun je
- Uitleggen wat assertion rewriting is en waar het wel en niet van toepassing is
- Een foutrapport regel voor regel lezen:
>,E,where,file:lineen de korte samenvatting - Kiezen tussen
==,in,is None,is Trueen een kaleassert x - Lijst-, dict- en string-diffs lezen, en om de volledige diff vragen met
-vv - Een bericht toevoegen aan een assert, en de tuple vermijden die altijd waar is
- De vorm van een rapport bepalen met
--tb=short,--tb=line,--tb=noen-l
Voorkennis: pytest installeren en je eerste test.
Voordat je de test schrijft
Een assert is een opgeschreven belofte. Voordat je er een intypt, moeten drie vragen worden beantwoord, en geen daarvan heeft met pytest te maken.
Wat wordt er precies beloofd? Neem de functie waarmee dit hoofdstuk afsluit, summarise(lines), die factuurregels leest zoals "pen, 12, 15.0". Het contract in één zin: hij slaat lege regels over en retourneert een dict met het aantal artikelen, hun namen in de opgegeven volgorde, en het totaal afgerond op twee decimalen. Elke assert die je later schrijft is één deel van die zin. Als je die zin niet kunt uitspreken, ben je nog niet klaar om de test te schrijven — je zou dan immers testen wat de code toevallig doet.
Wat moet er al zijn? Heel weinig voor dit hoofdstuk: de virtuele omgeving uit hoofdstuk één waarin pytest is geïnstalleerd (python -m pytest --version geeft antwoord), en de te testen module die geïmporteerd kan worden vanaf de plek waar je pytest uitvoert — hier staat invoice.py naast test_invoice.py en voer je pytest uit vanuit die map. Geen bestanden, geen netwerk, geen fixtures.
Welke testgevallen zijn er, en wat verwacht elk geval? Reken de verwachte waarden met de hand uit, voordat je de code uitvoert. 12 × 15.0 + 2 × 850.0 is 1880.0; dat getal komt voort uit rekenkunde, niet uit wat summarise toevallig printte. Als je in plaats daarvan de uitvoer naar de test kopieert, legt de test het gedrag van vandaag vast, inclusief bugs, en zal hij die bug voortaan verdedigen.
| Geval | Invoer | Verwacht | Assert die je zult schrijven | | --- | --- | --- | --- | | Eén regel, spaties rondom de naam | " pen , 12, 15.0" | {"name": "pen", "qty": 12, "price": 15.0} | == op de hele dict | | Lege regels worden overgeslagen | ["pen, 12, 15.0", "", "bag, 2, 850.0"] | aantal is 2 | == | | De volledige samenvatting | dezelfde drie regels | {"count": 2, "names": ["pen", "bag"], "total": 1880.0} | == op de hele dict | | Een artikel opzoeken dat bestaat | "pen" | een record, niet None | is not None | | Een artikel opzoeken dat niet bestaat | lege lijst, "pen" | None | is None | | Het totaal is logisch | dezelfde drie regels | groter dan 0 | > met een bericht |
De laatste kolom is waar dit hoofdstuk over gaat: voor elke belofte de vergelijking die deze het meest precies formuleert en die faalt met het nuttigste rapport.
Wat je niet moet testen. Dat str.split splitst, dat round afrondt, dat int("12") gelijk is aan 12 — Python test die zaken zelf al. De exacte formulering van de uitvoer van pytest hoef je evenmin te testen. Een misvormde regel zoals "pen, twelve, 15.0" hoort weliswaar thuis in het plan, maar om te controleren of er een exception optreedt is pytest.raises nodig, en dat komt aan bod in hoofdstuk vier; schrijf de rij nu op en schrijf de test pas dan.
Hoe pytest binnenin een assert kijkt
Gewone Python gooit de waarden weg op het moment dat een assert faalt. Pytest bewaart ze omdat het je testbestand aanpast voordat het wordt uitgevoerd.
Wanneer pytest een testmodule importeert, zoekt het elk assert-statement op en herschrijft het dit naar code die elke tussenliggende waarde opslaat — het resultaat van full_name(...), de string aan de rechterkant — voordat de voorwaarde wordt gecontroleerd. Wanneer de check faalt, vormen die opgeslagen waarden het rapport. Dit is assertion rewriting, en het is de reden waarom pytest geen assertEqual of assertIn nodig heeft: een gewone assert met een gewone == is voldoende.
Schakel het eens uit om te zien wat het voor je doet. pytest -q --assert=plain op dezelfde test:
def test_full_name():
> assert full_name("ada", "lovelace") == "Ada Lovelace "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E AssertionError
test_names.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_names.py::test_full_name - AssertionErrorWeer terug bij één enkel, kaal woord. Rewriting is van toepassing op testbestanden (test_*.py, *_test.py) en op conftest.py. Het is niet van toepassing op je andere modules, wat van belang wordt zodra je een assert in een helperfunctie zet — "Wanneer het misgaat" laat zien hoe dat eruitziet.
Een foutrapport regel voor regel lezen
Een kleine module, cart.py:
def subtotal(items):
return sum(price * qty for price, qty in items)
def total(items, discount):
return subtotal(items) - discountEn test_cart.py, waarin de tweede test het verkeerde getal verwacht:
from cart import subtotal, total
def test_subtotal():
assert subtotal([(15.0, 2), (60.0, 1)]) == 90.0
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
assert total(items, 10) == 70.0Voer gewoon pytest uit:
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
collected 2 items
test_cart.py .F [100%]
=================================== FAILURES ===================================
___________________________ test_total_with_discount ___________________________
def test_total_with_discount():
items = [(15.0, 2), (60.0, 1)]
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
test_cart.py:10: AssertionError
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
========================= 1 failed, 1 passed in 0.01s ==========================Lees het vanaf de bovenkant:
test_cart.py .F— één teken per test, op volgorde..geslaagd,Fmislukt.____ test_total_with_discount ____— de kop van één mislukking. Elke falende test krijgt zijn eigen sectie.- De broncoderegels — de body van de test tot aan de regel die faalde.
>— de exacte regel die de fout veroorzaakte. Alles erboven werd zonder problemen uitgevoerd.E assert 80.0 == 70.0— opnieuw de assert, waarbij elke expressie is vervangen door zijn waarde. De linkerkant was80.0, de rechterkant70.0.E + where 80.0 = total(...)— waar een waarde vandaan kwam. Bij een functieaanroep toont pytest de aanroep met de argumenten ingevuld. Geneste aanroepen leveren genestewhere-regels op, elk steeds verder ingesprongen.test_cart.py:10: AssertionError— bestand en regelnummer, klaar om naartoe te springen in je editor, en het type exception.short test summary info— één regel per mislukking,FAILED path::test_name - first line of the error. Lees bij veel mislukkingen deze lijst als eerste.- De laatste regel — de tellingen en de tijd.
Waarom in deze volgorde lezen? Omdat elke regel het zoekgebied verkleint. De samenvatting vertelt je welke tests; de >-regel welke bewering; de E-regel welke kant afwijkt; de where-regel welke aanroep dit heeft opgeleverd. Pas daarna open je de code — en je opent meteen de juiste functie.
Soms luidt de eerste E-regel AssertionError: assert ... en soms alleen assert .... Het is dezelfde mislukking; het voorvoegsel blijft staan wanneer de waarden aanhalingstekens bevatten.
De vergelijkingen die je het vaakst schrijft
Elke expressie kan achter assert staan, en pytest licht de gangbare varianten toe. users.py:
def find_user(users, email):
for user in users:
if user["email"] == email:
return user
return None
def active_emails(users):
return [u["email"] for u in users if u["active"]]Vier tests, vier soorten vergelijkingen:
from users import active_emails, find_user
ADA = {"email": "[email protected]", "active": True}
ALAN = {"email": "[email protected]", "active": False}
def test_equal():
assert active_emails([ADA, ALAN]) == ["[email protected]", "[email protected]"]
def test_in():
assert "[email protected]" in active_emails([ADA, ALAN])
def test_is_not_none():
assert find_user([ADA], "[email protected]") is not None
def test_greater():
assert len(active_emails([ADA, ALAN])) > 1Alle vier mislukken ze. De >- en E-regels van elk, geknipt uit één run van pytest -q:
> assert active_emails([ADA, ALAN]) == ["[email protected]", "[email protected]"]
E AssertionError: assert ['[email protected]'] == ['ada@[email protected]']
E
E Right contains one more item: '[email protected]'
E Use -v to get more diff
> assert "[email protected]" in active_emails([ADA, ALAN])
E AssertionError: assert '[email protected]' in ['[email protected]']
E + where ['[email protected]'] = active_emails([{'email': '[email protected]', 'active': True}, {'email': '[email protected]', 'active': False}])
> assert find_user([ADA], "[email protected]") is not None
E AssertionError: assert None is not None
E + where None = find_user([{'email': '[email protected]', 'active': True}], '[email protected]')
> assert len(active_emails([ADA, ALAN])) > 1
E AssertionError: assert 1 > 1
E + where 1 = len(['[email protected]'])
E + where ['[email protected]'] = active_emails([{'email': '[email protected]', 'active': True}, {'email': '[email protected]', 'active': False}])Elk vertelt iets anders:
==vergelijkt waarden. Bij lijsten vertelt pytest ook hoe ze verschillen — de rechterkant heeft één item meer. (active_emailsheeft gelijk door Alan weg te laten; de verwachting van deze test klopte niet.)incontroleert lidmaatschap, en toont de hele container zodat je kunt zien wat er in plaats daarvan in stond.is None/is not Nonecontroleert identiteit met het uniekeNone-object.find_userretourneerdeNoneomdat het opzoeken hoofdlettergevoelig is.<,>,<=,>=werken zoals opgeschreven. Tweewhere-regels:1kwam vanlen(...), en die lijst weer vanactive_emails(...).
Waarom doet die keuze ertoe, als elke vorm toch kan mislukken? Omdat het rapport wordt opgebouwd uit de expressie die jij hebt geschreven. De verkeerde manier om te vragen "staat Alan in de lijst?":
def test_count():
emails = ["[email protected]"]
assert emails.count("[email protected]") > 0> assert emails.count("[email protected]") > 0
E AssertionError: assert 0 > 0
E + where 0 = <built-in method count of list object at 0x7d9189cec640>('[email protected]')
E + where <built-in method count of list object at 0x7d9189cec640> = ['[email protected]'].countDe kopregel is assert 0 > 0, en de lijst waar het je om gaat zit begraven aan het einde van de tweede where. De juiste manier, assert "[email protected]" in emails, plaatst de lijst juist in de kopregel. Schrijf de assert zoals de zin die je in gedachten had, en pytest legt de mislukking in dezelfde bewoordingen uit.
Truthiness: assert x is niet assert x == True
assert x slaagt zodra x truthy is — alles behalve False, None, 0, "" en lege containers. Vaak is dat precies wat je wilt; soms verbergt het een bug.
rules.py bevat twee functies. overdue klopt; is_adult bevat een bug — hij retourneert strings in plaats van booleans:
def overdue(invoices):
return [i for i in invoices if i["days"] > 30]
def is_adult(age):
if age >= 18:
return "yes"
return "no"from rules import is_adult, overdue
def test_finds_overdue():
assert overdue([{"id": 1, "days": 12}, {"id": 2, "days": 30}])
def test_child_is_not_adult():
assert not is_adult(12)
def test_adult_truthy():
assert is_adult(40)
def test_adult_is_true():
assert is_adult(40) is Truepytest -q, waarbij de drie mislukkingen zijn teruggebracht tot hun E-regels:
FF.F [100%]
E AssertionError: assert []
E + where [] = overdue([{'id': 1, 'days': 12}, {'id': 2, 'days': 30}])
E AssertionError: assert not 'no'
E + where 'no' = is_adult(12)
E AssertionError: assert 'yes' is True
E + where 'yes' = is_adult(40)
=========================== short test summary info ============================
FAILED test_rules.py::test_finds_overdue - AssertionError: assert []
FAILED test_rules.py::test_child_is_not_adult - AssertionError: assert not 'no'
FAILED test_rules.py::test_adult_is_true - AssertionError: assert 'yes' is True
3 failed, 1 passed in 0.01sDe eerste mislukking: zelfs een kale assert some_list krijgt een nuttig rapport. assert [] vertelt dat de lijst leeg terugkwam, niet verkeerd. Dertig dagen is niet meer dan dertig, dus de code klopt en de gegevens van de test waren verkeerd — geleerd uit één regel.
Dan de punt in FF.F. test_adult_truthy is geslaagd. is_adult retourneert de string "yes", wat truthy is, dus werd aan assert is_adult(40) voldaan door een functie die een bug bevat. "no" is eveneens truthy, en daarom ontdekte assert not is_adult(12) het wel — maar puur door toeval, afhankelijk van welk geval je toevallig testte.
Dus, assert x == True? Dat ontdekt "yes", maar heeft zelf ook een zwakke plek: in Python geldt 1 == True en 0 == False. stock.py:
def in_stock(qty):
return qty and qty > 0from stock import in_stock
def test_zero_equals_false():
assert in_stock(0) == False
def test_zero_is_false():
assert in_stock(0) is False.F [100%]
> assert in_stock(0) is False
E assert 0 is False
E + where 0 = in_stock(0)
1 failed, 1 passed in 0.01sqty and qty > 0 retourneert 0 wanneer qty gelijk is aan 0, niet False. De test met == slaagde; de test met is merkte het op. De regel die hieruit volgt:
assert x/assert not xwanneer elke truthy of falsy waarde acceptabel is — "de lijst is niet leeg", "er is geen foutmelding".assert x is True/assert x is Falsewanneer de functie een echteboolbelooft.assert x == Truebijna nooit. Het is minder strikt danis Trueen zegt minder dan een kaleassert x; linters markeren dit.
Wanneer de waarden groot zijn: diffs
Voor korte waarden vertelt assert 80.0 == 70.0 het hele verhaal. Bij een lijst van veertig items of een e-mail van drie regels vergelijkt pytest de twee kanten en toont het alleen wat verschilt.
def test_list():
got = ["pen", "notebook", "bag", "eraser"]
assert got == ["pen", "notebook", "ruler", "eraser"]
def test_dict():
got = {"id": 7, "name": "Ada", "role": "admin", "active": True}
assert got == {"id": 7, "name": "Ada", "role": "editor", "active": True}
def test_multiline_string():
got = "Dear Ada,\nYour order 7 has shipped.\nThanks!"
assert got == "Dear Ada,\nYour order 7 has been shipped.\nThanks!"De E-regels van de drie mislukkingen:
E AssertionError: assert ['pen', 'note...ag', 'eraser'] == ['pen', 'note...er', 'eraser']
E
E At index 2 diff: 'bag' != 'ruler'
E Use -v to get more diff
E AssertionError: assert {'id': 7, 'na...active': True} == {'id': 7, 'na...active': True}
E
E Omitting 3 identical items, use -vv to show
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E Use -v to get more diff
E AssertionError: assert 'Dear Ada,\nY...ped.\nThanks!' == 'Dear Ada,\nY...ped.\nThanks!'
E
E Dear Ada,
E - Your order 7 has been shipped.
E ? -----
E + Your order 7 has shipped.
E Thanks!De eerste E-regel is ingekort met ... om te passen; de regels eronder bevatten het detail:
- Lijsten — de eerste index waarop ze verschillen, of welke kant extra items heeft.
- Dicts — de keys waarvan de waarden verschillen. Identieke keys worden geteld, niet geprint.
- Strings — een diff regel voor regel. Regels die beginnen met twee spaties zijn aan beide kanten gelijk.
+markeert de linkerkant van==,-de rechterkant. De?-regel eronder wijst de exacte tekens aan.
Dus als je altijd assert actual == expected schrijft, is + wat je code opleverde en - wat je wilde. Als je de volgorde tussen tests door elkaar haalt, moet je telkens even stilstaan om uit te zoeken wat wat is; houd je die volgorde aan, dan hoeft dat nooit.
Een lange string op één regel krijgt dezelfde behandeling, waarbij het identieke deel wordt overgeslagen:
def test_receipt_line():
got = "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2420.00"
assert got == "2026-10-07 | order 1042 | 3 items | paid by card | ships to Dhaka | total 2402.00"E AssertionError: assert '2026-10-07 |...total 2420.00' == '2026-10-07 |...total 2402.00'
E
E Skipping 66 identical leading characters in diff, use -v to show
E - | total 2402.00
E ? -
E + | total 2420.00
E ? +Twee verwisselde cijfers aan het einde van een regel van 80 tekens, voor je gevonden.
-v en -vv: om de volledige diff vragen
Let op de hints: Use -v to get more diff, use -vv to show. Standaard kapt pytest lange toelichtingen af, en elke -v verhoogt de verbositeit. De dict-test met pytest -vv:
E AssertionError: assert {'id': 7, 'name': 'Ada', 'role': 'admin', 'active': True} == {'id': 7, 'name': 'Ada', 'role': 'editor', 'active': True}
E
E Common items:
E {'active': True, 'id': 7, 'name': 'Ada'}
E Differing items:
E {'role': 'admin'} != {'role': 'editor'}
E
E Full diff:
E {
E 'id': 7,
E 'name': 'Ada',
E - 'role': 'editor',
E ? ^ ^^^
E + 'role': 'admin',
E ? ^ + ^
E 'active': True,
E }Niets is ingekort: beide dicts volledig, de gemeenschappelijke items, en een diff met één key per regel. Waarom zou je niet altijd -vv gebruiken? Omdat bij een lijst van tweehonderd records de volledige diff tweehonderd regels telt, terwijl de standaardsamenvatting — "at index 2" — precies was wat je nodig had. Begin met de standaard; voeg -vv toe voor die ene test waarvan de samenvatting niet volstaat.
Je eigen bericht: assert cond, "msg"
Na een komma kan een assert een bericht meekrijgen, dat pytest boven zijn eigen toelichting print:
def test_everything_in_stock():
stock = {"pen": 12, "bag": 0}
for name, qty in stock.items():
assert qty > 0, f"{name} is out of stock"> assert qty > 0, f"{name} is out of stock"
E AssertionError: bag is out of stock
E assert 0 > 0Zonder het bericht zou je assert 0 > 0 zien en moeten uitzoeken welk artikel nul was. Een bericht verdient zijn plek wanneer de waarden alleen niet vertellen welk geval faalde of waarom de regel bestaat. Het verkeerde gebruik is herhalen wat pytest al toont: "expected 70.0 but got 80.0" voegt niets toe aan assert 80.0 == 70.0.
De tuple-valkuil
Wanneer een bericht de regel lang maakt, is het verleidelijk om het geheel tussen haakjes te zetten:
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong"). [100%]
=============================== warnings summary ===============================
test_trap.py:3
/home/you/shop/test_trap.py:3: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
assert (total == 70.0, "total is wrong")
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 passed, 1 warning in 0.01sDe test is geslaagd, hoewel total gelijk is aan 80.0. Haakjes met een komma erin bouwen een tuple van twee items, en een niet-lege tuple is altijd truthy — de assert controleert de tuple, niet de vergelijking. Pytest waarschuwt wel, maar een waarschuwing onderaan een lange run zie je gemakkelijk over het hoofd. Verwijder de haakjes:
def test_total():
total = 80.0
assert total == 70.0, "total is wrong"> assert total == 70.0, "total is wrong"
E AssertionError: total is wrong
E assert 80.0 == 70.0Als de regel te lang is, zet dan alleen haakjes om het bericht:
def test_total():
total = 80.0
assert total == 70.0, (
"total is wrong after applying the loyalty discount"
)Eén assert per gedrag
Een test die velden na elkaar controleert, stopt bij het eerste veld dat faalt. orders.py bevat twee bugs — de naam wordt niet gestript en het aantal blijft een string:
def parse_order(line):
order_id, customer, qty, price = line.split(",")
return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}De verkeerde manier, en daarna de juiste manier:
from orders import parse_order
def test_parse_order_fields():
order = parse_order("1042, Ada ,3,15.0")
assert order["id"] == 1042
assert order["customer"] == "Ada"
assert order["qty"] == 3
assert order["price"] == 15.0
def test_parse_order():
order = parse_order("1042, Ada ,3,15.0")
assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}> assert order["customer"] == "Ada"
E AssertionError: assert ' Ada ' == 'Ada'
E
E - Ada
E + Ada
E ? + +
> assert order == {"id": 1042, "customer": "Ada", "qty": 3, "price": 15.0}
E AssertionError: assert {'id': 1042, ...'price': 15.0} == {'id': 1042, ...'price': 15.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'customer': ' Ada '} != {'customer': 'Ada'}
E {'qty': '3'} != {'qty': 3}
E Use -v to get more diffDe eerste test meldt één bug. Je lost hem op, voert de test opnieuw uit, en stuit pas dan op de tweede. De tweede test vergelijkt het volledige resultaat en meldt beide tegelijk.
De richtlijn is één gedrag per test, niet één assert per test. "Het parsen van een regel levert dit record op" is één gedrag, dus één vergelijking van de volledige waarde is ideaal. Meerdere asserts zijn prima wanneer ze samen één gedrag beschrijven. Wat je moet vermijden is één test die het parsen controleert, daarna het overslaan van lege regels, en daarna het optellen — drie gedragingen, waarbij de eerste mislukking de andere twee verbergt en de naam van de test niet kan aangeven wat er stuk is gegaan.
Hoeveel rapport: --tb en -l
Het gedeelte tussen de kop en file:line is de traceback, en met --tb kies je hoeveel daarvan wordt getoond. De test van cart.py met pytest -q --tb=short:
___________________________ test_total_with_discount ___________________________
test_cart.py:10: in test_total_with_discount
assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)Alleen de falende regel, niet de hele functie. --tb=line geeft één locatie per mislukking:
E assert 80.0 == 70.0
+ where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
/home/you/shop/test_cart.py:10: assert 80.0 == 70.0En --tb=no laat alleen de samenvatting over:
.F [100%]
=========================== short test summary info ============================
FAILED test_cart.py::test_total_with_discount - assert 80.0 == 70.0
1 failed, 1 passed in 0.01sDe standaardwaarde, --tb=auto, is de lange vorm die je het hele hoofdstuk hebt gezien. Gebruik --tb=no of --tb=line om te zien wat er faalde in een grote run; voer daarna één test opnieuw uit met de standaardinstelling om te zien waarom.
Andersom voegt -l (--showlocals) de lokale variabelen toe van de falende functie. pytest -q -l:
> assert total(items, 10) == 70.0
E assert 80.0 == 70.0
E + where 80.0 = total([(15.0, 2), (60.0, 1)], 10)
items = [(15.0, 2), (60.0, 1)]
test_cart.py:10: AssertionErrorIn een test met een lus of meerdere tussenliggende variabelen is dat extra blok vaak precies wat de mislukking verklaart — je zult het aan het einde zien in de oplossing.
Een compleet voorbeeld
invoice.py leest regels zoals "pen, 12, 15.0":
def parse_line(line):
name, qty, price = line.split(",")
return {"name": name.strip(), "qty": int(qty), "price": float(price)}
def summarise(lines):
items = [parse_line(line) for line in lines if line.strip()]
total = sum(item["qty"] * item["price"] for item in items)
return {
"count": len(items),
"names": [item["name"] for item in items],
"total": round(total, 2),
}
def find_item(items, name):
for item in items:
if item["name"] == name:
return item
return Nonetest_invoice.py is het plan uit "Voordat je de test schrijft", één rij per test, elk geschreven met de vergelijking uit de laatste kolom van de tabel:
from invoice import find_item, parse_line, summarise
LINES = ["pen, 12, 15.0", "", "bag, 2, 850.0"]
def test_parse_line_strips_and_converts():
assert parse_line(" pen , 12, 15.0") == {"name": "pen", "qty": 12, "price": 15.0}
def test_blank_lines_are_skipped():
assert summarise(LINES)["count"] == 2
def test_summary():
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
def test_find_item_present():
items = [parse_line("pen, 12, 15.0")]
assert find_item(items, "pen") is not None
def test_find_item_missing():
assert find_item([], "pen") is None
def test_total_is_positive():
total = summarise(LINES)["total"]
assert total > 0, f"total should be positive, got {total}"pytest -q:
...... [100%]
6 passed in 0.01sNu "ruimt" iemand summarise wat op zodat de namen gesorteerd terugkomen, door één regel te veranderen in "names": sorted(item["name"] for item in items),. pytest -q --tb=short:
..F... [100%]
=================================== FAILURES ===================================
_________________________________ test_summary _________________________________
test_invoice.py:15: in test_summary
assert summarise(LINES) == {"count": 2, "names": ["pen", "bag"], "total": 1880.0}
E AssertionError: assert {'count': 2, ...otal': 1880.0} == {'count': 2, ...otal': 1880.0}
E
E Omitting 2 identical items, use -vv to show
E Differing items:
E {'names': ['bag', 'pen']} != {'names': ['pen', 'bag']}
E Use -v to get more diff
=========================== short test summary info ============================
FAILED test_invoice.py::test_summary - AssertionError: assert {'count': 2, .....
1 failed, 5 passed in 0.01sLees het op de manier die dit hoofdstuk je heeft geleerd. Eén van de zes tests is mislukt, test_summary, op regel 15. De eerste E-regel is ingekort, dus het detail staat eronder: twee keys kwamen overeen, en names verschilt — ['bag', 'pen'] uit de code (links) tegenover ['pen', 'bag'] in de test (rechts). De volgorde is veranderd, verder niets. Of dat een bug is of een bewuste wijziging, is nu een beslissing over facturen, niet een zoektocht door de code.
Let er ook op welke tests niet faalden. De telling, het opzoeken en het totaal slagen nog steeds, dus het rapport beperkt de wijziging tot één key van één dict. Dat is de opbrengst van één gedrag per test: het patroon van geslaagde en mislukte tests is op zichzelf al een diagnose.
Wanneer het misgaat
E AssertionError zonder waarden, in een helpermodule Rewriting geldt alleen voor testbestanden en conftest.py. Een assert in bijvoorbeeld checks.py levert een kale AssertionError op. Registreer de module voordat deze wordt geïmporteerd, bovenaan conftest.py: pytest.register_assert_rewrite("checks"). Het rapport toont dan zoals gebruikelijk assert 0 > 0.
PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses? Je hebt assert (condition, "message") geschreven. Dat is een tuple, en de test kan nooit falen. Verwijder de buitenste haakjes; zet alleen haakjes om het bericht als de regel lang is.
AssertionError: assert None == ... of + where None = ... De functie die je aanriep, retourneerde None. Meestal bevat deze een pad zonder return, of wijzigt deze iets ter plekke en retourneert niets — list.sort() is het klassieke voorbeeld.
Use -v to get more diff / ...Full output truncated (8 lines hidden), use '-vv' to show Geen foutmelding — pytest heeft de uitleg ingekort. Voer die ene test opnieuw uit met -vv.
Een test die slaagt terwijl de waarde overduidelijk verkeerd is Kijk of er sprake is van assert x waarbij x een niet-lege string is zoals "no", assert x == True waarbij x gelijk is aan 1, of de tuple-valkuil. Van een test die nog nooit is mislukt, is nog nooit bewezen dat hij werkt — breek de code een keer met opzet en zie hem rood worden.
Stap 4 van 6 — Voorspellen
Test je begrip
De test faalt. Welke uitlegregel print pytest onder de eerste E-regel?
def test_names():
got = ["pen", "bag"]
assert got == ["pen", "bag", "ink"]- Aassert 2 == 3
- BRight contains one more item: 'ink'
- CAt index 2 diff: 'ink' != None
- DLeft contains one more item: 'ink'
total is 80.0, en toch slaagt deze test. Waarom?
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong")- ADe bericht-string neemt de plaats in van de voorwaarde
- B`80.0 == 70.0` is waar voor floats
- CDe haakjes bouwen een tuple van twee items, en een niet-lege tuple is altijd waar
- Dpytest slaat asserts over die een bericht bevatten
Je schrijft altijd assert actual == expected. Welke regels zijn wat in een string-diff: degene die beginnen met + en degene die beginnen met -?
- A`+` is de verwachte waarde, `-` is wat de code opleverde
- BHet hangt ervan af welke waarde langer is
- C`+` is wat de code opleverde (linkerkant), `-` is de verwachte waarde (rechterkant)
- D`+` markeert toegevoegde tekens in beide waarden, `-` verwijderde tekens in beide
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 grades.py met vier functies:
letter(score)—"A"vanaf 80,"B"vanaf 65,"C"vanaf 50, anders"F"passed(score)— een echtebool,Truevanaf 50report(scores)— accepteert een dict van namen naar scores, retourneert een dict van namen naar lettersbest(scores)— de naam met de hoogste score, ofNonebij een lege dict
Schrijf eerst het testplan op als een tabel, net zoals dit hoofdstuk deed: geval, invoer, verwacht, en de assert die je zult gebruiken. Schrijf daarna test_grades.py met ten minste zes tests. Gebruik elk van deze ten minste één keer: == op een hele dict, in, is None, is True of is False, een vergelijking zoals >=, en een assert met een bericht dat het falende testgeval noemt.
Breek daarna met opzet dingen en lees elk rapport:
- Laat
passedde expressiescore >= 50 and scoreretourneren in plaats van een bool. Welke tests merken dit op? - Verplaats de grens voor
"B"van 65 naar 66. Welke tests merken dit op, welke niet — en waarom? - Zet een van je asserts samen met het bericht tussen haakjes. Zoek de waarschuwing op.
- Voer het bestand, met break 2 actief, uit met
--tb=line, en daarna de falende test met-l.
Schrijf voor elke break de eerste regel van het rapport op die je vertelde wat er mis was.
Oplossing
Eerst het plan. Grenzen zijn de plek waar beoordelingscode de fout in gaat, dus elke grens krijgt beide kanten — 80 en 79, 65 en 64, 50 en 49:
| Geval | Invoer | Verwacht | Assert | | --- | --- | --- | --- | | Elke grens, beide kanten | 80, 79, 65, 64, 50, 49 | A, B, B, C, C, F | == met een bericht dat de score noemt | | passed is een echte bool | 50, 49 | True, False | is True, is False | | Volledig rapport | vier studenten | de hele dict van letters | == op de dict | | Iemand is gezakt | dezelfde vier | "F" tussen de letters | in | | best is echt de beste | dezelfde vier | geen score boven die van de winnaar | >= met een bericht | | Niemand om te rangschikken | {} | None | is None |
grades.py:
def letter(score):
if score >= 80:
return "A"
if score >= 65:
return "B"
if score >= 50:
return "C"
return "F"
def passed(score):
return score >= 50
def report(scores):
return {name: letter(score) for name, score in scores.items()}
def best(scores):
if not scores:
return None
return max(scores, key=scores.get)test_grades.py:
from grades import best, letter, passed, report
SCORES = {"ada": 91, "alan": 64, "grace": 50, "linus": 49}
def test_letter_boundaries():
cases = [(80, "A"), (79, "B"), (65, "B"), (64, "C"), (50, "C"), (49, "F")]
for score, expected in cases:
assert letter(score) == expected, f"letter({score})"
def test_passed_returns_a_real_bool():
assert passed(50) is True
assert passed(49) is False
def test_report():
assert report(SCORES) == {"ada": "A", "alan": "C", "grace": "C", "linus": "F"}
def test_a_failing_letter_appears():
assert "F" in report(SCORES).values()
def test_best_has_the_highest_score():
top = best(SCORES)
for name, score in SCORES.items():
assert SCORES[top] >= score, f"{name} scored more than {top}"
def test_best_of_nobody():
assert best({}) is Nonepytest -q:
...... [100%]
6 passed in 0.01sBreak 1 — passed retourneert score >= 50 and score:
.F.... [100%]
> assert passed(50) is True
E assert 50 is True
E + where 50 = passed(50)
FAILED test_grades.py::test_passed_returns_a_real_bool - assert 50 is True
1 failed, 5 passed in 0.01s50 is truthy, dus assert passed(50) zou dit hebben doorgelaten. is True ontdekte het, en assert 50 is True noemt de waarde die een bool had moeten zijn.
Break 2 — de grens voor "B" verplaatst naar 66, met --tb=line:
F..... [100%]
=================================== FAILURES ===================================
E AssertionError: letter(65)
assert 'C' == 'B'
- B
+ C
/home/you/grades/test_grades.py:9: AssertionError: letter(65)
=========================== short test summary info ============================
FAILED test_grades.py::test_letter_boundaries - AssertionError: letter(65)
1 failed, 5 passed in 0.01sAlleen de grenstest merkte het op. test_report slaagde nog steeds omdat geen enkele student in SCORES precies 65 heeft — een test dekt alleen de waarden die je erin stopt, en dat is waarom het plan beide kanten van elke grens opnam. Het bericht letter(65) staat vooraan, en dat is nodig: de lus voert zes testgevallen uit op dezelfde regel, en zonder het bericht zou je wel weten dat er een letter verkeerd was, maar niet bij welke score. + C is wat de code opleverde, - B wat de test verwachtte.
Dezelfde mislukking met pytest -q -l test_grades.py::test_letter_boundaries eindigt met de variabelen van de lus:
cases = [(80, 'A'), (79, 'B'), (65, 'B'), (64, 'C'), (50, 'C'), (49, 'F')]
expected = 'B'
score = 65Break 3 — assert (best({}) is None, "no scores, no best") op regel 32:
...... [100%]
/home/you/grades/test_grades.py:32: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
6 passed, 1 warning in 0.01sZes geslaagd — en één daarvan kan niet meer falen, wat best ook retourneert. De waarschuwing is het enige signaal.
De eerste regels die je vertelden wat er mis was: assert 50 is True, AssertionError: letter(65), en assertion is always true. Naar regels zoals deze zoek je vanaf nu als eerste bij elke mislukking.
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