pytest installieren und dein erster Test
Warum manuelles Prüfen an seine Grenzen stößt, warum ein einfaches assert fast nichts verrät und wie du pytest installierst sowie eine erste Testdatei schreibst und ausführst. Einschließlich der Punkte, der Zusammenfassungszeile, -q, -v und Exit-Codes.
Wie möchtest du lernen?
Das Problem, das wir lösen
Hier ist eine kleine Funktion und die Art und Weise, wie die meisten von uns eine solche Funktion überprüfen – ausführen und sich ansehen, was herauskommt:
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.0Du liest die drei Zahlen, rechnest im Kopf nach und entscheidest, dass sie stimmen. Das ist ein Test. Ein manueller Test, und er hat zwei Probleme.
Das erste ist, dass er nicht skaliert. Drei Zahlen lassen sich leicht mit dem Auge prüfen. Dreißig Funktionen mit jeweils fünf Fällen, die nach jeder Änderung erneut überprüft werden, nicht – und niemand macht das.
Das zweite ist noch schlimmer. Einen Monat später räumt jemand die Funktion auf:
def discounted(price, percent):
return price * (1 - percent // 100)$ python -c "from prices import discounted; print(discounted(200, 10))"
200Es sieht vernünftig aus und läuft ohne Fehler durch. Aber // ist eine Ganzzahldivision, also ist 10 // 100 gleich 0, und jeder Rabatt ist still und leise verschwunden. Code, der früher funktionierte und es nun nicht mehr tut, nennt man eine Regression, und genau das übersieht manuelles Prüfen – weil niemand zurückgeht, um die alten Zahlen noch einmal zu kontrollieren.
Die Lösung besteht darin, die erwarteten Ergebnisse im Code aufzuschreiben, damit eine Maschine sie jedes Mal überprüfen kann. Python hat dafür bereits eine Anweisung: assert. Lege die Prüfungen in einer Datei namens check_prices.py neben prices.py ab:
from prices import discounted
assert discounted(200, 10) == 180.0
assert discounted(80, 25) == 60.0
assert discounted(99, 0) == 99
print("all good")Mit der ursprünglichen Funktion:
$ python check_prices.py
all goodMit der „aufgeräumten“:
$ 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorDie Regression wurde abgefangen, was ein Fortschritt ist. Aber sieh dir an, was dir mitgeteilt wird: AssertionError und sonst nichts. Nicht, was discounted(200, 10) tatsächlich zurückgegeben hat. Und das Skript hat beim ersten Fehlschlag abgebrochen, sodass du nicht weißt, ob die anderen beiden Prüfungen bestehen.
In diesem Kapitel installieren wir das Werkzeug, das beides behebt: pytest.
Am Ende dieses Kapitels kannst du
- Erklären, warum automatisierte Tests Regressionen abfangen, die manuelles Prüfen übersieht
- pytest mit
uvoderpipinstallieren und mitpytest --versionbestätigen - Eine Testdatei schreiben, die pytest von selbst findet, und sie ausführen
- Die Punkte und die Zusammenfassungszeile lesen und zwischen
-qund-vwechseln - Sagen, was die Exit-Codes
0und1bedeuten und warum das wichtig ist
Voraussetzungen: Bevor du einen Test schreibst. Du brauchst außerdem Python 3 und musst wissen, wie man eine Funktion schreibt.
Bevor du den Test schreibst
Im vorherigen Kapitel ging es darum zu entscheiden, was getestet werden soll. In diesem geht es um die konkreten Dinge, die vorhanden sein müssen, bevor die erste Zeile Testcode ausgeführt werden kann. Es gibt vier Punkte, und jeder davon erzeugt, wenn er fehlt, seinen ganz eigenen verwirrenden Fehler – es lohnt sich also, sie der Reihe nach durchzugehen.
1. Ein Projektordner, eine Umgebung. Alles in diesem Kapitel befindet sich in einem einzigen Ordner:
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 testsWarum eine eigene Umgebung? Weil pytest in demselben Python installiert sein muss, das auch deinen Code importieren wird. Wenn pytest in einer Python-Installation liegt und die Pakete deines Projekts in einer anderen, erhältst du Fehler, die nach fehlendem Code aussehen, obwohl das eigentliche Problem ein fehlendes Werkzeug ist. Ein .venv pro Projekt erspart dir das Rätselraten.
2. pytest installiert in dieser Umgebung – das erledigt der nächste Abschnitt.
3. Der Code ist importierbar. Eine Testdatei ist ganz gewöhnliches Python: Ihre erste Zeile wird from prices import discounted lauten. Überprüfe daher vor dem Schreiben eines Tests, ob genau dieser Import aus dem Projektordner heraus funktioniert:
$ python -c "from prices import discounted; print(discounted(200, 10))"
180.0Wenn diese Zeile fehlschlägt, schlägt pytest genauso fehl – nur verpackt in einen längeren Bericht. Dies separat zu prüfen dauert fünf Sekunden und trennt „mein Code wird nicht gefunden“ von „mein Code ist falsch“. Das bedeutet auch, dass der Modulname ein gültiger Python-Name sein muss: prices.py kann importiert werden, my-prices.py und 2prices.py nicht.
4. Das Versprechen, aufgeschrieben als Fälle. Bevor du einen Test tippst, schreibe auf, was discounted verspricht, eine Zeile pro Fall:
| Fall | Eingabe | Erwartet | | --- | --- | --- | | Typischer Rabatt | discounted(200, 10) | 180.0 | | Ein zweiter, anderer Rabatt | discounted(80, 25) | 60.0 | | Randfall: überhaupt kein Rabatt | discounted(99, 0) | 99 |
Und genauso bewusst: was nicht auf der Liste steht. Ein negativer Prozentsatz oder einer über 100: Darüber macht die Funktion bisher kein Versprechen, und ein Test würde lediglich festschreiben, was auch immer sie heute zufällig tut. (Ungültige Eingaben mit einem Fehler abzuweisen, ist ein eigenes Versprechen – Kapitel vier.) Pythons eigene Arithmetik ebenfalls: Du testest deine Formel, nicht, dass * multipliziert.
Warum zwei gewöhnliche Fälle und nicht nur einer? Sieh dir die dritte Zeile im Vergleich zur „aufgeräumten“ Funktion vom Anfang des Kapitels an: 99 * (1 - 0 // 100) ist immer noch 99. Der Null-Prozent-Fall besteht selbst mit dem Bug. Ein Plan, der nur aus Randfällen besteht, kann genau den Fehler übersehen, auf den es ankommt; und ein Plan aus nur einem typischen Fall übersieht die Ränder. Jede Zeile ist da, um etwas abzufangen, was die anderen nicht erfassen würden.
Der Rest des Kapitels verwandelt diese Tabelle in eine Testdatei.
pytest installieren
pytest ist kein Bestandteil von Python; es ist ein Paket, das du in deinem Projekt installierst. Es ist ein Werkzeug für die Entwicklung des Codes, nichts, was das Programm bei der Ausführung benötigt – daher kommt es als Entwicklungs-Abhängigkeit hinein.
Wenn dein Projekt uv verwendet, führe dies im Projektordner aus:
$ 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 trägt es in pyproject.toml unter einer dev-Gruppe ein, sodass jeder, der das Projekt später einrichtet, dasselbe Werkzeug erhält:
[dependency-groups]
dev = [
"pytest>=9.1.1",
]Wenn du normales pip in einer virtuellen Umgebung verwendest, aktiviere die Umgebung und führe Folgendes aus:
$ python -m pip install pytestpip gibt den Download-Fortschritt aus und endet mit:
Successfully installed iniconfig-2.3.1 packaging-26.3 pluggy-1.6.0 pygments-2.21.0 pytest-9.1.1python -m pip statt nur pip stellt sicher, dass das Paket in genau dem Python landet, das du auch ausführen wirst – eine häufige Ursache für „Ich habe es installiert, aber es ist nicht da“.
Überprüfe nun, ob es geklappt hat:
$ pytest --version
pytest 9.1.1Ein Hinweis, bevor es weitergeht: Bei uv werden Befehle innerhalb der Umgebung des Projekts ausgeführt, wenn ihnen uv run vorangestellt wird – also uv run pytest --version. Bei einer aktivierten virtuellen Umgebung genügt einfaches pytest. Der Rest dieses Kurses schreibt einfaches pytest; setze uv run davor, wenn du so arbeitest.
Deine erste Testdatei
Zwei Benennungsregeln, mehr braucht pytest nicht von dir:
- der Dateiname beginnt mit
test_(oder endet auf_test.py) - jede Funktion, die ausgeführt werden soll, beginnt mit
test
Stelle die ursprüngliche, korrekte prices.py wieder her und erstelle daneben test_prices.py:
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) == 99Jede Prüfung aus check_prices.py ist nun eine eigene kleine Funktion, und jeder Name sagt aus, was geprüft wird. Es gibt kein print, keine Liste zu registrierender Tests, kein if __name__ == "__main__". Führe nun pytest aus dem Projektordner heraus aus:
$ 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 hat den Ordner durchsucht, test_prices.py aufgrund des Namens gefunden, die drei Funktionen aufgrund ihrer Namen entdeckt, jede einzelne ausgeführt und Bericht erstattet.
Die Ausgabe lesen
Gehen wir sie Zeile für Zeile durch.
platform ...— welches Python und welches pytest liefen. Einen Blick wert, wenn sich Ergebnisse zwischen zwei Rechnern unterscheiden.rootdir: /home/you/shop— der Ordner, den pytest als oberste Projektebene betrachtet.collected 3 items— wie viele Tests es gefunden hat. Das Finden von Tests nennt man Collection (Sammlung). Ist diese Zahl niedriger als erwartet, stimmt irgendwo ein Name nicht.test_prices.py ...— ein Zeichen pro Test in der Reihenfolge, in der sie liefen. Ein.bedeutet bestanden.[100%]— der Fortschritt über den gesamten Durchlauf.3 passed in 0.01s— die Zusammenfassungszeile. Das ist die Zeile, die man als Erstes liest.
Setze nun das „aufgeräumte“ discounted wieder ein und führe es erneut aus. Diesmal mit -q, das wir gleich noch genauer kennenlernen werden:
$ 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.01sDie Fortschrittszeile lautet nun FF.: ein F für jeden Fehlschlag. Und vergleiche das mit dem bloßen AssertionError von vorhin:
- jeder Test lief — ein Fehlschlag in einem hat die anderen nicht gestoppt, sodass du sehen kannst, dass null Prozent immer noch funktioniert
- jeder Fehlschlag nennt den Test und die Zeile
assert 200 == 180.0zeigt den Wert, den die Funktion tatsächlich zurückgegeben hat
Diesen Bericht im Detail zu lesen, ist das Thema des gesamten nächsten Kapitels. Behalte für den Moment nur im Kopf, dass sich die Zusammenfassungszeile zu 2 failed, 1 passed geändert hat – und dass dasselbe assert, das du bereits kanntest, sich nun selbst erklärt.
-q und -v: weniger und mehr
Die Standardausgabe ist ein Mittelweg. Zwei Flags verschieben sie in die eine oder andere Richtung.
-q (quiet) lässt den Kopfbereich weg:
$ pytest -q
... [100%]
3 passed in 0.01s-v (verbose) gibt jedem Test eine eigene Zeile mit seinem vollständigen Namen:
$ 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 ist die Node-ID des Tests: die Datei, zwei Doppelpunkte, die Funktion. Du wirst sie später verwenden, um einen einzelnen Test auszuführen. Hier zahlen sich auch beschreibende Testnamen aus – in einer Auflistung mit -v verrät dir test_quarter_off, was bestanden hat; test_2 würde das nicht tun.
Verwende -q, wenn du Tests ständig ausführst und nur das Gesamtergebnis wissen willst, und -v, wenn du genau sehen möchtest, welche Tests liefen.
Was ist .pytest_cache? Ein Ordner, den pytest anlegt, um sich Dinge zwischen Durchläufen zu merken, etwa welche Tests beim letzten Mal fehlschlugen. Er kann problemlos gelöscht werden und gehört nicht in die Versionsverwaltung.Exit-Codes: Wie eine Maschine das Ergebnis liest
Du liest die Zusammenfassungszeile. Ein Skript, ein Editor oder ein Build-Server können das nicht – sie lesen den Exit-Code, also die Zahl, die jedes Programm an die Shell zurückgibt, wenn es beendet wird. In einer Unix-Shell enthält $? den letzten Wert:
$ pytest -q
... [100%]
3 passed in 0.01s
$ echo $?
0Und mit der fehlerhaften Funktion nach dem Durchlauf, der mit 2 failed, 1 passed endete:
$ echo $?
10— alles gefunden, alles bestanden1— Tests liefen und mindestens einer schlug fehl
(In PowerShell heißt die Variable $LASTEXITCODE.) Dadurch können Tests automatisch ausgeführt werden, bevor Code zusammengeführt wird: Ein Exit-Code ungleich null stoppt den Prozess. Es gibt noch ein paar weitere Codes, von denen du einen weiter unten unter „Wenn etwas schiefgeht“ kennenlernen wirst.
Wo Testdateien liegen
In diesem Kapitel liegt die Testdatei direkt neben dem Code:
shop/
├── prices.py
└── test_prices.pyFür eine Handvoll Dateien ist das völlig in Ordnung. Die meisten Projekte sammeln Tests stattdessen in einem eigenen tests/-Ordner. In beiden Fällen findet pytest sie anhand ihres Namens; wie man ein echtes Projekt strukturiert und was sich dabei ändert, ist Thema von Kapitel drei.
Ein vollständiges Beispiel
Eine Funktion, die eine Punktzahl in eine Schulnote umwandelt, 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"Und ihre Tests, 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 ===============================Zwei Dinge sind bemerkenswert.
Erstens die Tests bei 80 und 79. Diese liegen auf beiden Seiten einer Grenze, und genau dort verstecken sich Fehler. Ändere score >= 80 zu score > 80 – ein leichtes Versehen – und führe die Quiet-Variante aus:
$ 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.01sDer zweite Test, test_exactly_eighty_is_still_an_a, ist derjenige, der fehlschlägt: 80 erhält nun ein 'B'. Ein Test nur bei 95 hätte das niemals bemerkt.
Zweitens die Namen. In der Auflistung von -v liest sich jede Zeile wie ein Satz über das Verhalten der Funktion. Wenn in ein paar Monaten einer davon fehlschlägt, verrät dir der Name allein schon, welches Versprechen gebrochen wurde.
Wenn etwas schiefgeht
zsh: command not found: pytest (oder bash: pytest: command not found) Die Shell kann das Programm pytest nicht finden. Entweder ist es nicht installiert, oder es ist in einer virtuellen Umgebung installiert, die nicht aktiv ist. Führe mit uv den Befehl uv run pytest aus; aktiviere mit pip zuerst die Umgebung. python -m pytest funktioniert ebenfalls und führt pytest mit demjenigen Python aus, auf das python verweist.
/usr/bin/python3: No module named pytest Du hast python -m pytest ausgeführt, und dieses Python hat kein pytest. Der Pfad am Anfang zeigt dir, welches Python es war – hier das System-Python, nicht das deines Projekts. Installiere pytest in der Umgebung, die du verwenden möchtest.
ModuleNotFoundError: No module named 'price' während der Collection Die Testdatei konnte deinen Code nicht importieren – hier wegen eines Tippfehlers, from price import ... statt 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.01sBeachte das Wort ERROR, nicht FAILED: Es wurde überhaupt kein Test ausgeführt, weil die Datei nicht einmal geladen werden konnte, und der Exit-Code ist 2. Lies die letzte E-Zeile und führe dann die Import-Prüfung aus „Bevor du den Test schreibst“ aus – python -c "from prices import discounted" –, bis sie für sich allein funktioniert.
no tests ran in 0.01s pytest lief, hat aber nichts gefunden (collected). Der Dateiname beginnt nicht mit test_ (tests_prices.py, prices_tests.py und test.py werden alle übersehen) oder die Funktionen beginnen nicht mit test (check_quarter_off wird stillschweigend ignoriert). Der Exit-Code lautet hier 5, nicht 0, damit ein automatisierter Durchlauf „nichts gefunden“ nicht mit „alle bestanden“ verwechselt.
Ein Test besteht, obwohl er es offensichtlich nicht sollte Halte nach einem fehlenden assert Ausschau:
from prices import discounted
def test_quarter_off():
discounted(80, 25) == 999$ pytest -q
. [100%]
1 passed in 0.01sDer Vergleich wird berechnet und verworfen. Ein Test schlägt nur dann fehl, wenn darin etwas eine Exception (Ausnahme) auslöst; ohne assert passiert das nicht.
PytestReturnNotNoneWarning: Test functions should return None Du hast return discounted(99, 0) == 99 anstelle von assert ... geschrieben. pytest ignoriert, was ein Test zurückgibt, daher besteht dieser Test unabhängig davon, ob der Vergleich wahr oder falsch ist. Die Warnung fragt sogar: Did you mean to use assert instead of return?
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Diese Datei wird als test_double.py gespeichert und pytest -q wird ausgeführt. Welche Zeichen stehen am Anfang der ersten Zeile der Ausgabe?
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 ist offensichtlich das falsche Ergebnis, doch pytest meldet 1 passed. Warum?
def discounted(price, percent):
return price - price * percent / 100
def test_quarter_off():
discounted(80, 25) == 999- Apytest prüft nur, ob die Funktion ausgeführt wurde, nicht deren Ergebnis
- BDie Zeile enthält kein `assert` — der Vergleich wird berechnet und verworfen
- CDezimalzahlen können nicht mit `==` verglichen werden
- DDer Funktionsname beginnt nicht mit `test_`
Eine Datei mit korrekten Tests heißt double_checks.py. Was passiert, wenn du pytest in diesem Ordner ausführst?
- Apytest bricht mit einem `ImportError` ab
- B`no tests ran` — der Dateiname beginnt nicht mit `test_`, wird also gar nicht erst durchsucht; Exit-Code `5`
- CJeder Test wird ausgeführt, weil die Funktionsnamen mit `test_` beginnen
- Dpytest benennt die Datei automatisch in `test_double_checks.py` um
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 einen Ordner mit einer Datei temperature.py, die zwei Funktionen enthält:
c_to_f(celsius)— gibtcelsius * 9 / 5 + 32zurückf_to_c(fahrenheit)— gibt(fahrenheit - 32) * 5 / 9zurück
Installiere dann pytest in der Umgebung dieses Ordners und bestätige dies mit pytest --version.
Schreibe test_temperature.py mit mindestens fünf Tests. Berücksichtige den Gefrierpunkt von Wasser (0 und 32), den Siedepunkt von Wasser (100 und 212) und -40, was auf beiden Skalen denselben Wert hat. Gib jedem Test einen Namen, der besagt, was er prüft.
Führe es auf drei Arten aus – pytest, pytest -q und pytest -v – und prüfe nach einer davon den Exit-Code.
Baue dann absichtlich einen Fehler ein: Ändere 9 / 5 zu 5 / 9 in c_to_f. Sage vor der Ausführung voraus, welche Tests fehlschlagen werden und wie die Fortschrittszeile aussehen wird. Führe pytest -q aus, vergleiche das Ergebnis und prüfe den Exit-Code erneut.
Benenne zuletzt test_temperature.py in temperature_check.py um und führe pytest aus. Lies, was ausgegeben wird, und prüfe den Exit-Code. Benenne die Datei danach wieder zurück. Zu wissen, wie „nichts gefunden“ aussieht, wird dir manchen verwirrenden Nachmittag ersparen.
Lösung
Zuerst der Plan, noch vor jeglichem Code – dieselbe Tabelle wie zuvor:
| Fall | Eingabe | Erwartet | | --- | --- | --- | | Wasser gefriert | c_to_f(0), f_to_c(32) | 32, 0 | | Wasser siedet | c_to_f(100), f_to_c(212) | 212, 100 | | Die beiden Skalen treffen sich | 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 / 9Vor dem Schreiben der Tests die Import-Prüfung aus dem Projektordner:
$ 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 ===============================Nun der absichtliche Bug, 9 / 5 geändert zu 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 $?
1Und die umbenannte Datei:
$ pytest -q
no tests ran in 0.01s
$ echo $?
5Warum es so geschrieben ist:
- Ein Verhalten pro Test, benannt als Satz. In der Auflistung mit
-vbesagt jede Zeile, was versprochen wird. Wenntest_water_boils_at_212_ffehlschlägt, weißt du bereits, was kaputt ist, bevor du überhaupt weiterliest. - Bekannte, nachprüfbare Werte.
0/32,100/212und-40sind Fakten, die du ohne Taschenrechner überprüfen kannst, sodass ein Fehlschlag auf den Code hinweist und nicht auf einen zweifelhaften Erwartungswert. - Beide Richtungen.
c_to_fundf_to_csind separater Code, daher benötigt jede Funktion ihre eigenen Tests; dass die eine korrekt ist, sagt nichts über die andere aus. - Der Durchlauf mit dem Bug ist der aufschlussreichste Teil. Nur zwei Tests schlugen fehl, und
test_water_freezes_at_32_fgehörte nicht dazu:0 * 5 / 9 + 32ergibt immer noch32, weil die Multiplikation mit null jeden Faktor verschluckt. Genau deshalb enthält der Plan mehr als einen Fall – eine Testsuite, die nur den Gefrierpunkt prüft, hätte den Test trotz des Bugs bestanden. Der Test für-40enthält zweiassert-Zeilen; die erste schlug fehl, sodass die zweite gar nicht erst ausgeführt wurde. Das ist hier vertretbar, weil beide dasselbe Versprechen beschreiben. - Die Exit-Codes —
0,1,5— sind die drei, denen du am häufigsten begegnen wirst: alles bestanden, etwas fehlgeschlagen, nichts gefunden.
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