Aufwärmen · Aufgabe 1 von 7
// I5.1 · ca. 30 Min. · Aufbau
Testen mit unittest
Nach dieser Lektion schreiben Sie unittest-Testfälle, die Ergebnisse und Ausnahmen prüfen, geben jedem Test mit setUp eine frische Fixture und führen eine Suite aus dem Code aus.
Lektion 1 von 5 in I5 Tests und Projektwerkzeuge
Danach können Sie
- Einen TestCase schreiben, dessen test_-Methoden assertEqual, assertTrue und assertRaises nutzen
- Mit setUp dafür sorgen, dass jeder Test mit einer frischen Fixture beginnt
- Eine Suite aus dem Code ausführen und testsRun, failures, errors und wasSuccessful() lesen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen: Wie viele Tests führt diese Suite aus?
import unittest class TestText(unittest.TestCase): def test_upper(self): self.assertEqual("abc".upper(), "ABC") def check_lower(self): self.assertEqual("ABC".lower(), "abc") suite = unittest.TestLoader().loadTestsFromTestCase(TestText) result = unittest.TestResult() suite.run(result) print(result.testsRun)Üben · Aufgabe 3 von 7
Setzen Sie die assert-Methode ein, damit der Test genau dann besteht, wenn int("ten") ValueError auslöst.
with self.____(ValueError): int("ten")with self.(ValueError):Üben · Aufgabe 4 von 7
setUp zählt seine Aufrufe an der Klasse. Was gibt das aus?
import unittest class TestCounter(unittest.TestCase): calls = 0 def setUp(self): TestCounter.calls += 1 self.items = [] def test_a(self): self.items.append(1) self.assertEqual(len(self.items), 1) def test_b(self): self.items.append(2) self.assertEqual(len(self.items), 1) def test_c(self): self.assertEqual(self.items, []) suite = unittest.TestLoader().loadTestsFromTestCase(TestCounter) result = unittest.TestResult() suite.run(result) print(TestCounter.calls, result.wasSuccessful())Üben · Aufgabe 5 von 7
Eine Suite hat drei Tests ausgeführt: einer bestand, bei einem scheiterte assertEqual, einer löste KeyError aus. Ordnen Sie jedem Teil des Ergebnisses seinen Wert zu.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. add() hat einen Fehler. Was gibt das aus?
import unittest def add(a, b): return a - b # bug class TestAdd(unittest.TestCase): def test_add(self): self.assertTrue(add(2, 3), 5) result = unittest.TestResult() unittest.TestLoader().loadTestsFromTestCase(TestAdd).run(result) print(result.wasSuccessful())Anwenden · Aufgabe 7 von 7
Kleine Aufgabe. Gegeben ist word_count(text), das len(text.split()) liefert. Schreiben Sie einen TestCase mit mindestens vier Tests: ein normaler Satz, der leere String, zusätzliche Leerzeichen und eine Zahl statt Text (int hat kein split, also AttributeError). Führen Sie die Suite aus und geben Sie testsRun und wasSuccessful() aus.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Einen Warenkorb testen
Ein Basket speichert Artikelpreise in Cent. Seine Tests teilen sich eine Fixture aus setUp, prüfen Summen mit assertEqual und Ausnahmen mit assertRaises. remove() hat einen Fehler: Es übergeht einen fehlenden Artikel, statt KeyError auszulösen. Das Programm führt die Suite in ein TestResult aus und gibt aus, was es gefunden hat, auch den gescheiterten Test. Reparieren Sie remove() mit del self.items[name] und starten Sie erneut.
main.py
import unittest
class Basket:
"""A shopping basket: item names mapped to prices in cents."""
def __init__(self) -> None:
self.items: dict[str, int] = {}
def add(self, name: str, cents: int) -> None:
if cents < 0:
raise ValueError(f"negative price: {cents}")
self.items[name] = cents
def remove(self, name: str) -> None:
self.items.pop(name, None) # bug: a missing item is ignored silently
def total(self) -> int:
return sum(self.items.values())
class TestBasket(unittest.TestCase):
def setUp(self) -> None:
# Runs before every test: each test gets a new basket with one item.
self.basket = Basket()
self.basket.add("tea", 350)
def test_total(self) -> None:
self.basket.add("cake", 425)
self.assertEqual(self.basket.total(), 775)
def test_remove(self) -> None:
self.basket.remove("tea")
self.assertEqual(self.basket.total(), 0)
def test_negative_price(self) -> None:
with self.assertRaises(ValueError):
self.basket.add("refund", -100)
def test_remove_missing(self) -> None:
with self.assertRaises(KeyError):
self.basket.remove("coffee")
suite = unittest.TestLoader().loadTestsFromTestCase(TestBasket)
result = unittest.TestResult()
suite.run(result)
print("tests run:", result.testsRun)
print("failures:", len(result.failures))
print("errors:", len(result.errors))
for test, trace in result.failures:
print("FAIL:", test.id())
print(" reason:", trace.strip().splitlines()[-1])
print("all passed:", result.wasSuccessful())
Ausführen mit
python main.pyAusgabe
tests run: 4
failures: 1
errors: 0
FAIL: __main__.TestBasket.test_remove_missing
reason: AssertionError: KeyError not raised
all passed: False- test_remove sieht nach dem Entfernen von tea die Summe 0: Der Kuchen aus test_total ist nicht in seinem Korb, weil setUp einen neuen gebaut hat.
- test.id() benennt den gescheiterten Test: Modul, Klasse und Methode.
- Die letzte Zeile des Tracebacks, „KeyError not raised“, ist die Meldung von assertRaises, wenn der Block normal endet.
- failures ist 1 und errors 0: Der Fehler lässt eine assert-Methode scheitern, er bringt den Test nicht zum Absturz.
Ändern und ausführen
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übungen
Übung 1 von 2
Tests, die Fehler finden
apply_discount ist korrekt. Ihre Aufgabe sind die Tests: Ergänzen Sie TestDiscount um mindestens drei Testmethoden. Prüfen Sie, dass 50 Prozent Rabatt auf 80.0 genau 40.0 ergeben und dass ein Prozentsatz über 100 und ein negativer Prozentsatz ValueError auslösen. Die Prüfungen lassen Ihre Tests gegen die echte Funktion laufen, die bestehen muss, und gegen drei fehlerhafte Versionen, die Ihre Tests erkennen müssen.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Ein neuer Test ist eine neue Methode der Klasse, deren Name mit test beginnt, zum Beispiel def test_half_price(self) -> None:.
Hinweis 2
Für den Wert: self.assertEqual(apply_discount(80.0, 50), 40.0).
Hinweis 3
Für jeden Fehlerfall: with self.assertRaises(ValueError): und eingerückt darunter der Aufruf, der auslösen muss.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import unittest
def apply_discount(price: float, percent: float) -> float:
"""Returns price reduced by percent, rounded to cents."""
if not 0 <= percent <= 100:
raise ValueError(f"percent must be 0-100, got {percent}")
return round(price * (100 - percent) / 100, 2)
class TestDiscount(unittest.TestCase):
def test_no_discount(self) -> None:
self.assertEqual(apply_discount(80.0, 0), 80.0)
def test_half_price(self) -> None:
self.assertEqual(apply_discount(80.0, 50), 40.0)
def test_percent_too_high(self) -> None:
with self.assertRaises(ValueError):
apply_discount(80.0, 120)
def test_negative_percent(self) -> None:
with self.assertRaises(ValueError):
apply_discount(80.0, -5)
if __name__ == "__main__":
suite = unittest.TestLoader().loadTestsFromTestCase(TestDiscount)
unittest.TextTestRunner(verbosity=2).run(suite)
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import unittest
def apply_discount(price: float, percent: float) -> float:
"""Returns price reduced by percent, rounded to cents."""
if not 0 <= percent <= 100:
raise ValueError(f"percent must be 0-100, got {percent}")
return round(price * (100 - percent) / 100, 2)
class TestDiscount(unittest.TestCase):
def test_no_discount(self) -> None:
self.assertEqual(apply_discount(80.0, 0), 80.0)
# Add three more tests:
# - 50 percent off 80.0 is 40.0
# - a percent above 100 raises ValueError
# - a negative percent raises ValueError
if __name__ == "__main__":
suite = unittest.TestLoader().loadTestsFromTestCase(TestDiscount)
unittest.TextTestRunner(verbosity=2).run(suite)
test_main.py
import unittest
import main
def run_against(implementation):
saved = main.apply_discount
main.apply_discount = implementation
try:
result = unittest.TestResult()
unittest.TestLoader().loadTestsFromTestCase(main.TestDiscount).run(result)
finally:
main.apply_discount = saved
return result
def subtracts_percent(price, percent):
if not 0 <= percent <= 100:
raise ValueError(percent)
return round(price - percent, 2)
def no_upper_limit(price, percent):
if percent < 0:
raise ValueError(percent)
return round(price * (100 - percent) / 100, 2)
def no_lower_limit(price, percent):
if percent > 100:
raise ValueError(percent)
return round(price * (100 - percent) / 100, 2)
def test_correct_code_passes():
"""Ihre Tests bestehen mit dem korrekten apply_discount"""
result = run_against(main.apply_discount)
failing = [test.id() for test, _ in result.failures + result.errors]
assert result.wasSuccessful(), f"diese Tests scheitern an korrektem Code: {failing}"
assert result.testsRun >= 4, f"{result.testsRun} Tests liefen; schreiben Sie mindestens 4"
def test_catches_wrong_formula():
"""Ihre Tests erkennen eine Formel, die den Prozentsatz als Geldbetrag abzieht"""
result = run_against(subtracts_percent)
assert not result.wasSuccessful(), "alle Tests bestehen, obwohl apply_discount(80.0, 50) 30.0 liefert; prüfen Sie einen echten Rabatt"
def test_catches_missing_upper_limit():
"""Ihre Tests erkennen eine fehlende Prüfung auf Prozentsätze über 100"""
result = run_against(no_upper_limit)
assert not result.wasSuccessful(), "alle Tests bestehen, obwohl apply_discount(80.0, 120) einen Preis liefert; prüfen Sie, dass ValueError ausgelöst wird"
def test_catches_missing_lower_limit():
"""Ihre Tests erkennen eine fehlende Prüfung auf negative Prozentsätze"""
result = run_against(no_lower_limit)
assert not result.wasSuccessful(), "alle Tests bestehen, obwohl apply_discount(80.0, -5) einen Preis liefert; prüfen Sie, dass ValueError ausgelöst wird"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Ein Testbericht in einer Zeile
Schreiben Sie summarize(case): Die Funktion nimmt eine TestCase-Klasse, führt alle ihre Tests aus und liefert eine Zeile wie "2 run, 1 failed, 0 errors: FAILED". Bestehen alle Tests, steht OK statt FAILED. Laden Sie die Tests mit unittest.TestLoader().loadTestsFromTestCase und führen Sie sie in ein unittest.TestResult aus. Ausführen zeigt den Bericht für TestDemo.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
suite = unittest.TestLoader().loadTestsFromTestCase(case), dann result = unittest.TestResult() und suite.run(result).
Hinweis 2
result.failures und result.errors sind Listen: Zählen Sie sie mit len().
Hinweis 3
status = "OK" if result.wasSuccessful() else "FAILED", dann die Zeile mit einem f-String bauen.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import unittest
def summarize(case: type[unittest.TestCase]) -> str:
suite = unittest.TestLoader().loadTestsFromTestCase(case)
result = unittest.TestResult()
suite.run(result)
status = "OK" if result.wasSuccessful() else "FAILED"
return f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errors: {status}"
class TestDemo(unittest.TestCase):
def test_ok(self) -> None:
self.assertEqual(1 + 1, 2)
def test_wrong(self) -> None:
self.assertEqual(1 + 1, 3)
if __name__ == "__main__":
print(summarize(TestDemo))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import unittest
def summarize(case: type[unittest.TestCase]) -> str:
# 1. Load the tests of case with unittest.TestLoader().loadTestsFromTestCase.
# 2. Run the suite into a unittest.TestResult.
# 3. Return "<run> run, <failed> failed, <errors> errors: OK",
# with FAILED instead of OK when a test did not pass.
return ""
class TestDemo(unittest.TestCase):
def test_ok(self) -> None:
self.assertEqual(1 + 1, 2)
def test_wrong(self) -> None:
self.assertEqual(1 + 1, 3)
if __name__ == "__main__":
print(summarize(TestDemo))
test_main.py
import unittest
from main import summarize
class AllPass(unittest.TestCase):
def test_one(self):
self.assertEqual(len("ab"), 2)
def test_two(self):
self.assertTrue("abc".startswith("a"))
class Mixed(unittest.TestCase):
def test_pass(self):
self.assertEqual(3 * 3, 9)
def test_fail(self):
self.assertEqual(3 * 3, 6)
def test_error(self):
int("nine")
class NoTests(unittest.TestCase):
def check_something(self):
self.assertEqual(1, 2)
def test_all_pass():
"""Zwei bestandene Tests ergeben OK"""
got = summarize(AllPass)
assert got == "2 run, 0 failed, 0 errors: OK", f"summarize lieferte {got!r}"
def test_mixed():
"""Ein Fehlschlag und ein Fehler werden getrennt gezählt"""
got = summarize(Mixed)
assert got == "3 run, 1 failed, 1 errors: FAILED", f"summarize lieferte {got!r}"
def test_no_tests():
"""Eine Klasse ohne test_-Methoden führt nichts aus"""
got = summarize(NoTests)
assert got == "0 run, 0 failed, 0 errors: OK", f"summarize lieferte {got!r}"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
Den alten Namen assertEquals verwenden
import sys
import unittest
class TestTotal(unittest.TestCase):
def test_sum(self):
self.assertEquals(sum([1, 2, 3]), 6)
suite = unittest.TestLoader().loadTestsFromTestCase(TestTotal)
result = unittest.TextTestRunner().run(suite)
sys.exit(0 if result.wasSuccessful() else 1)
Was Python ausgibt
AttributeError: 'TestTotal' object has no attribute 'assertEquals'. Did you mean: 'assertEqual'?Warum, und die Lösung
Ältere Anleitungen verwenden assertEquals, einen Alias, den neuere Python-Versionen nicht mehr haben. Die Methode heißt assertEqual, ohne s. Der Runner meldet den Test als Fehler (error), nicht als Fehlschlag, weil der Test abstürzte, bevor er etwas vergleichen konnte.
Die Funktion in assertRaises schon aufrufen
import sys
import unittest
class TestParse(unittest.TestCase):
def test_bad_number(self):
self.assertRaises(ValueError, int("x"))
suite = unittest.TestLoader().loadTestsFromTestCase(TestParse)
result = unittest.TextTestRunner().run(suite)
sys.exit(0 if result.wasSuccessful() else 1)
Was Python ausgibt
ValueError: invalid literal for int() with base 10: 'x'Warum, und die Lösung
int("x") läuft, bevor assertRaises überhaupt aufgerufen wird. Die Ausnahme entkommt, und der Test ist ein Fehler. Übergeben Sie entweder Funktion und Argumente getrennt, self.assertRaises(ValueError, int, "x"), damit assertRaises den Aufruf macht, oder nutzen Sie die with-Form: with self.assertRaises(ValueError): int("x").
Ein Tippfehler in TestCase
import unittest
class TestUpper(unittest.Testcase):
def test_upper(self):
self.assertEqual("abc".upper(), "ABC")
Was Python ausgibt
AttributeError: module 'unittest' has no attribute 'Testcase'. Did you mean: 'TestCase'?Warum, und die Lösung
Python unterscheidet Groß- und Kleinschreibung, und die Klasse heißt TestCase mit großem C. Der Fehler tritt auf, sobald die class-Anweisung läuft, noch vor jedem Test; deshalb kann Python den richtigen Namen vorschlagen.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.