assert und Fehlerberichte lesen
Wie pytest in ein einfaches assert hineinsieht, wie du einen Fehlerbericht Zeile für Zeile liest und welchen Vergleich du schreiben solltest, damit der Bericht dir am meisten verrät.
Wie möchtest du lernen?
Das Problem, das wir lösen
Hier ist eine Überprüfung in reinem Python, in einer Datei namens 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 sagt dir, dass die Behauptung falsch war, und sonst nichts. Was hat full_name tatsächlich zurückgegeben? Du müsstest ein print hinzufügen, es noch einmal ausführen und nachsehen.
Setze dieselbe Zeile in einen Test, test_names.py, und führe pytest -q aus:
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.01sJetzt siehst du beide Werte und eine Markierung unter dem einen Zeichen, das sich unterscheidet: ein nachgestelltes Leerzeichen im erwarteten String. Die Funktion hatte recht; der Test war falsch.
Ein fehlschlagender Test ist nur so nützlich wie das, was er dir verrät. In diesem Kapitel geht es darum, alles zu lesen, was pytest dir mitteilt, und Asserts so zu schreiben, dass pytest etwas Aussagekräftiges berichten kann.
Am Ende dieses Kapitels kannst du
- Erklären, was Assertion Rewriting ist und wo es greift (und wo nicht)
- Einen Fehlerbericht Zeile für Zeile lesen:
>,E,where,file:lineund die kurze Zusammenfassung - Zwischen
==,in,is None,is Trueund einem einfachenassert xwählen - Listen-, Dict- und String-Diffs lesen und mit
-vvden vollständigen Diff anfordern - Einem Assert eine Nachricht hinzufügen und das Tupel vermeiden, das immer wahr ist
- Einen Bericht mit
--tb=short,--tb=line,--tb=nound-lanpassen
Voraussetzungen: pytest installieren und dein erster Test.
Bevor du den Test schreibst
Ein assert ist ein schriftlich festgehaltenes Versprechen. Bevor du eines tippst, müssen drei Fragen beantwortet werden — und keine davon betrifft pytest.
Was genau wird versprochen? Nimm die Funktion, mit der dieses Kapitel endet, summarise(lines), die Rechnungszeilen wie "pen, 12, 15.0" liest. Ihr Vertrag in einem Satz: Sie überspringt Leerzeilen und gibt ein Dict mit der Anzahl der Artikel, deren Namen in der angegebenen Reihenfolge und der auf zwei Nachkommastellen gerundeten Gesamtsumme zurück. Jedes Assert, das du später schreibst, ist ein Teilsatz dieses Satzes. Wenn du den Satz nicht formulieren kannst, bist du noch nicht bereit, den Test zu schreiben — du würdest am Ende nur testen, was der Code zufällig gerade tut.
Was muss vorhanden sein? Für dieses Kapitel sehr wenig: die virtuelle Umgebung aus Kapitel eins mit installiertem pytest (python -m pytest --version gibt Auskunft), und das zu testende Modul muss von dem Ort aus importierbar sein, an dem du pytest ausführst — hier liegt invoice.py neben test_invoice.py, und du führst pytest aus diesem Ordner aus. Keine Dateien, kein Netzwerk, keine Fixtures.
Welche Fälle gibt es und was erwartet jeder davon? Berechne die erwarteten Werte von Hand, bevor du den Code ausführst. 12 × 15.0 + 2 × 850.0 ist 1880.0; diese Zahl stammt aus der Arithmetik, nicht aus der Ausgabe von summarise. Wenn du stattdessen die Ausgabe in den Test kopierst, hält der Test das heutige Verhalten inklusive aller Bugs fest und wird den Bug von da an verteidigen.
| Fall | Eingabe | Erwartet | Assert, das du schreiben wirst | | --- | --- | --- | --- | | Eine Zeile, Leerzeichen um den Namen | " pen , 12, 15.0" | {"name": "pen", "qty": 12, "price": 15.0} | == auf das gesamte Dict | | Leerzeilen werden übersprungen | ["pen, 12, 15.0", "", "bag, 2, 850.0"] | Anzahl ist 2 | == | | Die gesamte Zusammenfassung | dieselben drei Zeilen | {"count": 2, "names": ["pen", "bag"], "total": 1880.0} | == auf das gesamte Dict | | Nach einem vorhandenen Artikel suchen | "pen" | ein Datensatz, nicht None | is not None | | Nach einem nicht vorhandenen Artikel suchen | leere Liste, "pen" | None | is None | | Die Gesamtsumme ist plausibel | dieselben drei Zeilen | größer als 0 | > mit einer Nachricht |
Darum geht es in der letzten Spalte und in diesem Kapitel: für jedes Versprechen der Vergleich, der es am präzisesten ausdrückt und bei einem Fehlschlag den nützlichsten Bericht liefert.
Was man nicht testen sollte. Dass str.split teilt, dass round rundet, dass int("12") gleich 12 ist — das testet Python bereits selbst. Auch der genaue Wortlaut der Ausgabe von pytest ist nichts, was du testen solltest. Eine fehlerhafte Zeile wie "pen, twelve, 15.0" gehört zwar in den Plan, aber das Prüfen, ob ein Fehler ausgelöst wird, erfordert pytest.raises — das kommt in Kapitel vier. Notiere die Zeile jetzt und schreibe den Test dann.
Wie pytest in ein assert hineinsieht
Reines Python verwirft die Werte in dem Moment, in dem ein assert fehlschlägt. Pytest behält sie, weil es deine Testdatei vor der Ausführung modifiziert.
Wenn pytest ein Testmodul importiert, findet es jede assert-Anweisung und schreibt sie in Code um, der jeden Zwischenwert speichert — das Ergebnis von full_name(...), den String auf der rechten Seite —, bevor die Bedingung geprüft wird. Schlägt die Prüfung fehl, werden diese gespeicherten Werte zum Bericht. Das ist Assertion Rewriting, und genau deshalb braucht pytest kein assertEqual oder assertIn: Ein ganz normales assert mit einem ganz normalen == reicht aus.
Schalte es ab, um zu sehen, was es für dich tut. pytest -q --assert=plain auf demselben 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 - AssertionErrorZurück zu einem einzigen kahlen Wort. Rewriting greift bei Testdateien (test_*.py, *_test.py) und bei conftest.py. Es greift nicht bei deinen anderen Modulen, was wichtig wird, sobald du ein assert in einem Hilfsmodul platzierst — „Wenn etwas schiefgeht“ zeigt, wie das aussieht.
Einen Fehlerbericht Zeile für Zeile lesen
Ein kleines Modul, cart.py:
def subtotal(items):
return sum(price * qty for price, qty in items)
def total(items, discount):
return subtotal(items) - discountUnd test_cart.py, in dem der zweite Test die falsche Zahl erwartet:
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.0Führe normales pytest aus:
============================= 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 ==========================Lies es von oben nach unten:
test_cart.py .F— ein Zeichen pro Test, der Reihe nach..bestanden,Ffehlgeschlagen.____ test_total_with_discount ____— die Überschrift eines Fehlschlags. Jeder fehlschlagende Test erhält einen eigenen Abschnitt.- Die Quellcodezeilen — der Rumpf des Tests bis zu der Zeile, die fehlschlug.
>— die genaue Zeile, die die Exception ausgelöst hat. Alles darüber lief beanstandungslos durch.E assert 80.0 == 70.0— das Assert noch einmal, wobei jeder Ausdruck durch seinen Wert ersetzt wurde. Die linke Seite war80.0, die rechte Seite70.0.E + where 80.0 = total(...)— woher ein Wert stammte. Bei einem Funktionsaufruf zeigt pytest den Aufruf mit den eingesetzten Argumenten. Verschachtelte Aufrufe ergeben verschachteltewhere-Zeilen, die jeweils weiter eingerückt sind.test_cart.py:10: AssertionError— Datei und Zeile, bereit zum Anspringen in deinem Editor, und die Art der Exception (Ausnahme).short test summary info— eine Zeile pro Fehlschlag,FAILED path::test_name - first line of the error. Lies bei vielen Fehlschlägen diese Liste zuerst.- Die letzte Zeile — die Anzahlen und die Zeit.
Warum in dieser Reihenfolge lesen? Weil jede Zeile die Suche eingrenzt. Die Zusammenfassung verrät dir, welche Tests; die >-Zeile, welche Behauptung; die E-Zeile, welche Seite abweicht; die where-Zeile, welcher Aufruf den Wert erzeugt hat. Erst dann öffnest du den Code — und zwar direkt bei der richtigen Funktion.
Manchmal lautet die erste E-Zeile AssertionError: assert ... und manchmal einfach nur assert .... Es ist derselbe Fehlschlag; das Präfix bleibt stehen, wenn die Werte Anführungszeichen enthalten.
Die Vergleiche, die du am häufigsten schreiben wirst
Jeder beliebige Ausdruck kann auf assert folgen, und pytest erklärt die gängigen. 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 Arten von Vergleichen:
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 schlagen fehl. Die >- und E-Zeilen jedes einzelnen, gekürzt aus einem Durchlauf von 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}])Jeder sagt etwas anderes:
==vergleicht Werte. Bei Listen teilt pytest auch mit, wie sie sich unterscheiden — die rechte Seite hat ein Element mehr. (active_emailslässt Alan zu Recht aus; die Erwartung dieses Tests war falsch.)inprüft die Zugehörigkeit (Membership) und zeigt den gesamten Container, sodass du siehst, was stattdessen vorhanden war.is None/is not Noneprüft die Identität mit dem einzigenNone-Objekt.find_usergabNonezurück, weil die Suche die Groß-/Kleinschreibung beachtet.<,>,<=,>=funktionieren wie geschrieben. Zweiwhere-Zeilen:1stammte vonlen(...), und diese Liste vonactive_emails(...).
Warum spielt die Wahl eine Rolle, wenn sich doch jede Form zum Fehlschlagen bringen lässt? Weil der Bericht aus dem Ausdruck aufgebaut wird, den du geschrieben hast. Der falsche Weg, um zu fragen: „Ist Alan in der Liste?“:
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]'].countDie Hauptzeile lautet assert 0 > 0, und die Liste, um die es dir geht, ist am Ende des zweiten where vergraben. Der richtige Weg, assert "[email protected]" in emails, setzt die Liste direkt in die Hauptzeile. Schreibe das Assert genau als den Satz, den du im Kopf hattest, und pytest erklärt den Fehlschlag mit denselben Begriffen.
Truthiness: assert x ist nicht assert x == True
assert x besteht immer dann, wenn x truthy ist — alles außer False, None, 0, "" und leeren Containern. Oft ist genau das gewollt; manchmal verbirgt es einen Bug.
rules.py hat zwei Funktionen. overdue ist korrekt; is_adult hat einen Bug — die Funktion gibt Strings statt Booleans zurück:
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, wobei die drei Fehlschläge auf ihre E-Zeilen gekürzt sind:
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.01sDer erste Fehlschlag: Selbst ein einfaches assert some_list erhält einen nützlichen Bericht. assert [] besagt, dass die Liste leer zurückkam, nicht falsch. Dreißig Tage sind nicht mehr als dreißig, der Code ist also richtig und die Testdaten waren falsch — gelernt aus einer einzigen Zeile.
Nun der Punkt in FF.F. test_adult_truthy hat bestanden. is_adult gibt den String "yes" zurück, der truthy ist; assert is_adult(40) wurde also von einer fehlerhaften Funktion erfüllt. "no" ist ebenfalls truthy, weshalb assert not is_adult(12) den Fehler bemerkt hat — aber nur durch den glücklichen Zufall, welchen Fall du gerade getestet hast.
Also doch assert x == True? Das fängt "yes" ab, hat aber eine eigene Schwachstelle: In Python ist 1 == True und 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 gibt 0 zurück, wenn qty gleich 0 ist, nicht False. Der ==-Test hat bestanden; der is-Test hat es bemerkt. Daraus folgt die Regel:
assert x/assert not x, wenn jeder beliebige truthy- oder falsy-Wert akzeptabel ist — „die Liste ist nicht leer“, „es gibt keine Fehlermeldung“.assert x is True/assert x is False, wenn die Funktion einen echtenboolverspricht.assert x == Truefast nie. Es ist ungenauer alsis Trueund sagt weniger aus als ein einfachesassert x; Linter markieren es als Warnung.
Wenn die Werte groß sind: Diffs
Bei kurzen Werten ist assert 80.0 == 70.0 bereits die ganze Geschichte. Bei einer Liste von vierzig Elementen oder einer dreizeiligen E-Mail vergleicht pytest beide Seiten und zeigt nur das, was sich unterscheidet.
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!"Die E-Zeilen der drei Fehlschläge:
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!Die erste E-Zeile wird mit ... gekürzt, damit sie hineinpasst; die Zeilen darunter enthalten die Details:
- Listen — der erste Index, an dem sie sich unterscheiden, oder welche Seite zusätzliche Elemente enthält.
- Dicts — die Schlüssel, deren Werte sich unterscheiden. Identische Schlüssel werden gezählt, nicht ausgegeben.
- Strings — ein zeilenweiser Diff. Zeilen, die mit zwei Leerzeichen beginnen, sind auf beiden Seiten gleich.
+markiert die linke Seite von==,-die rechte. Die?-Zeile darunter zeigt auf die genauen Zeichen.
Wenn du also immer assert actual == expected schreibst, ist + das, was dein Code erzeugt hat, und - das, was du haben wolltest. Vermischst du die Reihenfolge zwischen Tests, musst du jedes Mal innehalten und herausfinden, was was ist; behältst du sie bei, musst du das nie tun.
Ein langer einzeiliger String wird genauso behandelt, wobei der identische Teil übersprungen wird:
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 ? +Zwei vertauschte Ziffern am Ende einer 80 Zeichen langen Zeile — direkt für dich gefunden.
-v und -vv: Den gesamten Diff anfordern
Beachte die Hinweise: Use -v to get more diff, use -vv to show. Standardmäßig kürzt pytest lange Erklärungen, und jedes -v erhöht die Ausführlichkeit (Verbosity). Der Dict-Test mit 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 }Nichts wird gekürzt: beide Dicts vollständig, die gemeinsamen Elemente und ein Diff mit einem Schlüssel pro Zeile. Warum sollte man nicht immer -vv verwenden? Weil bei einer Liste von zweihundert Datensätzen der vollständige Diff zweihundert Zeilen lang ist und die standardmäßige Zusammenfassung — „at index 2“ — genau das ist, was du brauchtest. Starte mit dem Standard; füge -vv für den einen Test hinzu, dessen Zusammenfassung nicht ausreicht.
Deine eigene Nachricht: assert cond, "msg"
Nach einem Komma kann ein assert eine Nachricht entgegennehmen, die pytest über der eigenen Erklärung ausgibt:
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 > 0Ohne die Nachricht würdest du assert 0 > 0 sehen und müsstest erst herausfinden, welcher Artikel null war. Eine Nachricht verdient ihren Platz, wenn die Werte allein nicht verraten, welcher Fall fehlschlug oder warum die Regel existiert. Der falsche Einsatz besteht darin, zu wiederholen, was pytest ohnehin schon anzeigt: "expected 70.0 but got 80.0" fügt assert 80.0 == 70.0 keinerlei neuen Mehrwert hinzu.
Die Tupel-Falle
Wenn eine Nachricht die Zeile lang macht, ist man versucht, das Ganze in Klammern zu setzen:
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.01sDer Test hat bestanden, obwohl total gleich 80.0 ist. Klammern mit einem Komma darin bilden ein Tupel aus zwei Elementen, und ein nicht-leeres Tupel ist immer truthy — das assert prüft das Tupel, nicht den Vergleich. Pytest warnt zwar, aber eine Warnung am Ende eines langen Durchlaufs wird leicht übersehen. Entferne die Klammern:
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.0Wenn die Zeile zu lang ist, setze Klammern nur um die Nachricht:
def test_total():
total = 80.0
assert total == 70.0, (
"total is wrong after applying the loyalty discount"
)Ein Assert pro Verhalten
Ein Test, der Felder nacheinander prüft, bricht beim ersten ab, das fehlschlägt. orders.py hat zwei Bugs — der Name wird nicht von Leerzeichen bereinigt (stripped) und die Anzahl bleibt ein String:
def parse_order(line):
order_id, customer, qty, price = line.split(",")
return {"id": int(order_id), "customer": customer, "qty": qty, "price": float(price)}Der falsche Weg, dann der richtige Weg:
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 diffDer erste Test meldet einen Bug. Du behebst ihn, führst die Tests erneut aus und stößt erst dann auf den zweiten. Der zweite Test vergleicht das Gesamtergebnis und meldet beide auf einmal.
Die Richtlinie lautet ein Verhalten pro Test, nicht ein assert pro Test. „Das Parsen einer Zeile erzeugt diesen Datensatz“ ist ein einzelnes Verhalten, daher ist ein Gesamtvergleich ideal. Mehrere Asserts sind völlig in Ordnung, wenn sie gemeinsam ein einziges Verhalten beschreiben. Was du vermeiden solltest, ist ein Test, der erst das Parsen prüft, dann das Überspringen von Leerzeilen, dann das Berechnen der Summe — drei Verhaltensweisen, bei denen der erste Fehlschlag die anderen beiden verbirgt und der Name des Tests nicht aussagen kann, was eigentlich kaputtging.
Wie viel Bericht: --tb und -l
Der Teil zwischen der Überschrift und file:line ist der Traceback, und --tb bestimmt, wie viel davon angezeigt wird. Der cart.py-Test mit 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)Nur die fehlschlagende Zeile, nicht die gesamte Funktion. --tb=line liefert eine Zeile pro Fehlschlag:
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.0Und --tb=no belässt es bei der reinen Zusammenfassung:
.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.01sDer Standardwert --tb=auto ist die ausführliche Form, die du im gesamten Kapitel gesehen hast. Verwende --tb=no oder --tb=line, um bei einem großen Durchlauf zu sehen, was fehlgeschlagen ist; führe dann einen einzelnen Test mit dem Standard aus, um zu sehen, warum.
In die andere Richtung fügt -l (--showlocals) die lokalen Variablen der fehlschlagenden Funktion hinzu. 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 einem Test mit einer Schleife oder mehreren Zwischenvariablen ist dieser zusätzliche Block oft derjenige, der den Fehlschlag erklärt — du wirst ihn am Ende in der Lösung sehen.
Ein vollständiges Beispiel
invoice.py liest Zeilen wie "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 ist der Plan aus „Bevor du den Test schreibst“, eine Zeile pro Test, jeweils geschrieben mit dem Vergleich aus der letzten Spalte der Tabelle:
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.01sNun „räumt“ jemand summarise auf, sodass die Namen sortiert zurückgegeben werden, und ändert eine Zeile 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.01sLies es so, wie es dieses Kapitel vermittelt hat. Einer von sechs Tests ist fehlgeschlagen, test_summary, in Zeile 15. Die erste E-Zeile ist gekürzt, die Details stehen also darunter: Zwei Schlüssel stimmten überein, und names weicht ab — ['bag', 'pen'] aus dem Code (links) gegenüber ['pen', 'bag'] im Test (rechts). Nur die Reihenfolge hat sich geändert, sonst nichts. Ob das ein Bug oder eine beabsichtigte Änderung ist, ist nun eine inhaltliche Entscheidung über Rechnungen, keine Fehlersuche im Code.
Beachte auch, welche Tests nicht fehlgeschlagen sind. Die Anzahl, die Suchen und die Gesamtsumme bestehen weiterhin; der Bericht grenzt die Änderung also auf einen einzigen Schlüssel eines einzelnen Dicts ein. Das ist der Lohn für ein Verhalten pro Test: Das Muster aus bestandenen und fehlgeschlagenen Tests ist bereits selbst eine Diagnose.
Wenn etwas schiefgeht
E AssertionError ohne Werte, aus einem Hilfsmodul Rewriting deckt nur Testdateien und conftest.py ab. Ein assert innerhalb von beispielsweise checks.py liefert ein einfaches AssertionError. Registriere das Modul vor dem Importieren ganz oben in conftest.py: pytest.register_assert_rewrite("checks"). Der Bericht zeigt dann wie gewohnt assert 0 > 0.
PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses? Du hast assert (condition, "message") geschrieben. Das ist ein Tupel, und der Test kann niemals fehlschlagen. Entferne die äußeren Klammern; setze Klammern nur um die Nachricht, falls die Zeile zu lang ist.
AssertionError: assert None == ... oder + where None = ... Die Funktion, die du aufgerufen hast, hat None zurückgegeben. Meist gibt es einen Ausführungspfad ohne return, oder sie verändert etwas an Ort und Stelle (in-place) und gibt nichts zurück — list.sort() ist der klassische Fall.
Use -v to get more diff / ...Full output truncated (8 lines hidden), use '-vv' to show Kein Fehler — pytest hat die Erklärung gekürzt. Führe diesen einen Test erneut mit -vv aus.
Ein Test, der besteht, obwohl der Wert offensichtlich falsch ist Halte Ausschau nach assert x, wobei x ein nicht-leerer String wie "no" ist, assert x == True, wobei x gleich 1 ist, oder nach der Tupel-Falle. Ein Test, der noch nie fehlgeschlagen ist, hat noch nie bewiesen, dass er funktioniert — baue einmal absichtlich einen Fehler in den Code ein und sieh zu, wie der Test rot wird.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Der Test schlägt fehl. Welche Erklärungszeile gibt pytest unter der ersten E-Zeile aus?
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 ist 80.0, doch dieser Test besteht. Warum?
def test_total():
total = 80.0
assert (total == 70.0, "total is wrong")- ADer Nachrichten-String nimmt den Platz der Bedingung ein
- B`80.0 == 70.0` ist für Floats wahr
- CDie Klammern erzeugen ein zwei-elementiges Tupel, und ein nicht-leeres Tupel ist immer wahr
- Dpytest überspringt Asserts, die eine Nachricht enthalten
Du schreibst immer assert actual == expected. Welche Zeilen sind was in einem String-Diff: die mit + und die mit - beginnenden?
- A`+` ist der erwartete Wert, `-` ist das, was der Code erzeugt hat
- BEs hängt davon ab, welcher Wert länger ist
- C`+` ist das, was der Code erzeugt hat (linke Seite), `-` ist der erwartete Wert (rechte Seite)
- D`+` markiert hinzugefügte Zeichen in beiden Werten, `-` entfernte in beiden
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Erstelle grades.py mit vier Funktionen:
letter(score)—"A"ab 80,"B"ab 65,"C"ab 50, andernfalls"F"passed(score)— ein echterbool,Trueab 50report(scores)— nimmt ein Dict von Namen zu Punktzahlen entgegen, gibt ein Dict von Namen zu Notenbuchstaben zurückbest(scores)— der Name mit der höchsten Punktzahl, oderNonebei einem leeren Dict
Schreibe zuerst den Testplan als Tabelle auf, genau wie in diesem Kapitel: Fall, Eingabe, Erwartet und das Assert, das du verwenden wirst. Schreibe dann test_grades.py mit mindestens sechs Tests. Verwende jedes dieser Elemente mindestens einmal: == auf ein ganzes Dict, in, is None, is True oder is False, einen Vergleich wie >= und ein Assert mit einer Nachricht, die den fehlschlagenden Fall benennt.
Baue danach absichtlich Fehler ein und lies die jeweiligen Berichte:
- Lass
passedscore >= 50 and scoreanstelle eines Booleans zurückgeben. Welche Tests bemerken das? - Verschiebe die
"B"-Grenze von 65 auf 66. Welche Tests bemerken das, welche nicht — und warum? - Setze eines deiner Asserts zusammen mit seiner Nachricht in Klammern. Finde die Warnung.
- Führe die Datei mit dem eingebauten Fehler 2 mit
--tb=lineaus, danach den fehlschlagenden Test mit-l.
Notiere für jeden Fehler die erste Zeile des Berichts, die dir verraten hat, was nicht stimmte.
Lösung
Zuerst der Plan. An Grenzen treten in Benotungscode am ehesten Fehler auf, daher bekommt jede Grenze beide Seiten — 80 und 79, 65 und 64, 50 und 49:
| Fall | Eingabe | Erwartet | Assert | | --- | --- | --- | --- | | Jede Grenze, beide Seiten | 80, 79, 65, 64, 50, 49 | A, B, B, C, C, F | == mit einer Nachricht, die die Punktzahl benennt | | passed ist ein echter bool | 50, 49 | True, False | is True, is False | | Gesamter Bericht | vier Personen | das gesamte Dict der Noten | == auf das Dict | | Jemand ist durchgefallen | dieselben vier | "F" unter den Noten | in | | best ist wirklich am besten | dieselben vier | keine Punktzahl über der des Siegers | >= mit einer Nachricht | | Niemand zu bewerten | {} | 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.01sFehler 1 — passed gibt score >= 50 and score zurück:
.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 ist truthy, also hätte assert passed(50) das durchgehen lassen. is True hat es abgefangen, und assert 50 is True benennt den Wert, der eigentlich ein bool hätte sein sollen.
Fehler 2 — die "B"-Grenze wurde auf 66 verschoben, mit --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.01sNur der Grenztest hat es bemerkt. test_report hat weiterhin bestanden, weil niemand in SCORES genau 65 Punkte hat — ein Test deckt nur die Werte ab, die du hineinsteckst, weshalb der Plan beide Seiten jeder Grenze aufgelistet hat. Die Nachricht letter(65) kommt zuerst und wird gebraucht: Die Schleife führt sechs Fälle in derselben Zeile aus, und ohne die Nachricht wüsstest du zwar, dass ein Buchstabe falsch war, aber nicht, für welche Punktzahl. + C ist das, was der Code zurückgegeben hat, - B das, was der Test erwartet hat.
Derselbe Fehlschlag mit pytest -q -l test_grades.py::test_letter_boundaries endet mit den Variablen der Schleife:
cases = [(80, 'A'), (79, 'B'), (65, 'B'), (64, 'C'), (50, 'C'), (49, 'F')]
expected = 'B'
score = 65Fehler 3 — assert (best({}) is None, "no scores, no best") in Zeile 32:
...... [100%]
/home/you/grades/test_grades.py:32: PytestAssertRewriteWarning: assertion is always true, perhaps remove parentheses?
6 passed, 1 warning in 0.01sSechs bestanden — und einer davon kann überhaupt nicht mehr fehlschlagen, ganz gleich, was best zurückgibt. Die Warnung ist das einzige Anzeichen.
Die ersten Zeilen, die dir verraten haben, was nicht stimmte: assert 50 is True, AssertionError: letter(65) und assertion is always true. Nach Zeilen wie diesen suchst du von nun an bei jedem Fehlschlag zuerst.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen