Zum Inhalt springen
aviral gupta

// 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

Anfang des Moduls

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus dem Modul über Ausnahmen: Welche dieser Aufrufe lösen ValueError aus? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. 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)
  3. Ü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):
  4. Ü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())
  5. Ü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.

  6. 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())
  7. 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.py

Ausgabe

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
  1. Hinweis 1

    Ein neuer Test ist eine neue Methode der Klasse, deren Name mit test beginnt, zum Beispiel def test_half_price(self) -> None:.

  2. Hinweis 2

    Für den Wert: self.assertEqual(apply_discount(80.0, 50), 40.0).

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.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
  1. Hinweis 1

    suite = unittest.TestLoader().loadTestsFromTestCase(case), dann result = unittest.TestResult() und suite.run(result).

  2. Hinweis 2

    result.failures und result.errors sind Listen: Zählen Sie sie mit len().

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Hä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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein TestCase und seine test_-Methoden

Ein Testfall ist eine Klasse, die von unittest.TestCase erbt. Jede Methode, deren Name mit test beginnt, ist ein Test; andere Methoden sind Helfer und laufen nie von allein. Im Test prüfen Sie Ergebnisse mit assert-Methoden: assertEqual(a, b) für gleiche Werte, assertTrue(x) für eine wahre Bedingung und assertRaises für eine Ausnahme. with self.assertRaises(ValueError): umschließt den Code, der auslösen muss. Scheitert eine assert-Methode, ist der Test ein Fehlschlag (failure); jede andere Ausnahme macht ihn zum Fehler (error).

setUp gibt jedem Test eine frische Fixture

Die Vorbereitung, die ein Test braucht, etwa ein neuer Warenkorb mit einem Artikel, heißt Fixture. Sie gehört in eine Methode setUp(self): unittest ruft sie vor jeder einzelnen Testmethode auf, jeweils an einer neuen Instanz. So hinterlässt kein Test Daten für den nächsten. tearDown(self) läuft nach jedem Test und räumt auf. Löst setUp selbst eine Ausnahme aus, zählt der Test als Fehler und seine Methode läuft nicht. Eine Liste als Klassenattribut teilen sich dagegen alle Tests.

Eine Suite aus dem Code ausführen

Auf Ihrem Rechner findet und startet python -m unittest die Testdateien. Im Code baut unittest.TestLoader().loadTestsFromTestCase(TestX) eine Suite aus allen Tests der Klasse. Führen Sie sie in ein unittest.TestResult aus oder mit unittest.TextTestRunner().run(suite); das gibt einen Bericht auf sys.stderr aus und liefert das Ergebnis. result.testsRun zählt die Tests, result.failures und result.errors enthalten Paare (Test, Traceback), und result.wasSuccessful() ist True, wenn nichts schiefging. sys.exit(0 if result.wasSuccessful() else 1) macht daraus einen Exit-Status.

Quellen

Zuletzt geprüft am 29. September 2026