Zum Inhalt springen
aviral gupta

// I5.3 · ca. 30 Min. · Aufbau

doctest und was man testet

Nach dieser Lektion schreiben Sie Docstring-Beispiele, die doctest prüft, wählen Randfälle wie leere Eingaben und Grenzen und teilen Tests so auf, dass jeder genau ein Verhalten prüft.

Lektion 3 von 5 in I5 Tests und Projektwerkzeuge

Danach können Sie

  • Docstring-Beispiele schreiben, auch mit erwarteter Ausnahme, und mit doctest ausführen
  • Randfälle wählen, die Fehler finden: leere Eingabe, ein einzelnes Element und beide Seiten einer Grenze
  • Fokussierte Tests schreiben, die je ein Verhalten prüfen und es im Namen nennen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der vorigen Lektion: Welche dieser Zeilen legen einen Mock an, der beim Aufruf ValueError auslöst? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Dieses Beispiel scheitert. Was zeigt doctest unter Got:?

    import doctest
    
    
    def greet(name):
        """
        >>> greet("Ada")
        Hello, Ada!
        """
        return f"Hello, {name}!"
    
    
    doctest.testmod()
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die doctest-Funktion ein, die jeden Docstring des laufenden Moduls prüft und die Zahlen liefert.

    print(doctest.____())
    print(doctest.())
  4. Üben · Aufgabe 4 von 7

    Das Traceback-Beispiel lässt den Stack weg. Was ist die letzte ausgegebene Zeile?

    import doctest
    
    
    def parse_age(text):
        """
        >>> parse_age("42")
        42
        >>> parse_age("old")
        Traceback (most recent call last):
        ValueError: not a number: 'old'
        """
        if not text.isdigit():
            raise ValueError(f"not a number: {text!r}")
        return int(text)
    
    
    print(doctest.testmod())
  5. Üben · Aufgabe 5 von 7

    Sie testen word_count(text), das len(text.split()) liefert. Ordnen Sie jeder Eingabe den Randfall zu, den sie abdeckt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Zwei Funktionen, drei Beispiele. Was ist die letzte ausgegebene Zeile?

    import doctest
    
    
    def add(a, b):
        """
        >>> add(1, 2)
        3
        >>> add(0.1, 0.2)
        0.3
        """
        return a + b
    
    
    def half(n):
        """
        >>> half(4)
        2.0
        """
        return n / 2
    
    
    print(doctest.testmod())
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. clamp(value, low, high) begrenzt value auf den Bereich low..high und löst ValueError aus, wenn low > high ist. Schreiben Sie die Docstring-Beispiele: ein Wert innerhalb, einer darunter, einer darüber, beide Grenzen genau und der Fehler. Beenden Sie die Datei auf Ihrem Rechner mit print(doctest.testmod()) und starten Sie sie.

    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

Eine Rechnung aufteilen

split_bill teilt eine Rechnung in Cent so gleichmäßig wie möglich auf. Der Docstring zeigt die typische Verwendung und den Fehler. Die Unit-Tests gehen an die Ränder: eine Summe, die nicht aufgeht, null Cent, mehr Personen als Cent und null Personen, ein Verhalten pro Test. Das Programm führt die Doctests der Funktion mit DocTestFinder und DocTestRunner aus, die auch testmod() nutzt, dann die Unit-Tests, und gibt beide Ergebnisse aus.

main.py

import doctest
import unittest


def split_bill(total_cents: int, people: int) -> list[int]:
    """Split a bill in cents as evenly as possible.

    >>> split_bill(1000, 4)
    [250, 250, 250, 250]
    >>> split_bill(1000, 3)
    [334, 333, 333]
    >>> split_bill(5, 1)
    [5]
    >>> split_bill(1000, 0)
    Traceback (most recent call last):
    ...
    ValueError: people must be at least 1
    """
    if people < 1:
        raise ValueError("people must be at least 1")
    share, rest = divmod(total_cents, people)
    return [share + 1 if i < rest else share for i in range(people)]


class TestSplitBill(unittest.TestCase):
    def test_shares_add_up_to_total(self) -> None:
        self.assertEqual(sum(split_bill(1000, 7)), 1000)

    def test_shares_differ_by_at_most_one_cent(self) -> None:
        shares = split_bill(1000, 7)
        self.assertTrue(max(shares) - min(shares) <= 1)

    def test_zero_total(self) -> None:
        self.assertEqual(split_bill(0, 3), [0, 0, 0])

    def test_more_people_than_cents(self) -> None:
        self.assertEqual(split_bill(2, 3), [1, 1, 0])

    def test_zero_people_raises(self) -> None:
        with self.assertRaises(ValueError):
            split_bill(1000, 0)


# doctest.testmod() would check every docstring of a module run as a script;
# here one function's examples are found and run, and the counts printed.
finder = doctest.DocTestFinder()
runner = doctest.DocTestRunner()
for test in finder.find(split_bill, "split_bill", globs=globals()):
    runner.run(test)
print("doctests:", runner.summarize(verbose=False))

result = unittest.TestResult()
unittest.TestLoader().loadTestsFromTestCase(TestSplitBill).run(result)
print("unit tests run:", result.testsRun)
print("all passed:", result.wasSuccessful())

Ausführen mit

python main.py

Ausgabe

doctests: TestResults(failed=0, attempted=4)
unit tests run: 5
all passed: True
  • Die vier Doctest-Beispiele dienen zugleich als Dokumentation: Man sieht sofort, was split_bill liefert.
  • Die Unit-Tests prüfen Eigenschaften wie „ergibt zusammen die Summe“, die für jede Eingabe gelten.
  • Jeder Testname sagt, welches Verhalten kaputt ist, wenn er scheitert, etwa test_more_people_than_cents.
  • Ändern Sie i < rest in i <= rest und starten Sie erneut: Die Doctests und mehrere fokussierte Tests scheitern.
Ä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

Beispiele für median

median(values) funktioniert. Der Docstring hat ein Beispiel; ergänzen Sie zwei: eine gerade Anzahl Werte, etwa [4, 1, 3, 2], bei der das Ergebnis der Mittelwert der beiden mittleren Werte ist, und eine leere Liste, die ValueError: median of an empty list auslöst. Die Prüfungen lassen Ihre Beispiele gegen das echte median laufen und gegen zwei fehlerhafte Versionen, die sie 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 Beispiel ist eine Zeile >>> median([4, 1, 3, 2]) und darunter, gleich eingerückt, die genaue Ausgabe: 2.5.

  2. Hinweis 2

    Für den Fehler besteht die erwartete Ausgabe aus drei Zeilen: Traceback (most recent call last):, dann ..., dann ValueError: median of an empty list.

  3. Hinweis 3

    Auf Ihrem eigenen Rechner führt doctest.testmod() am Ende der Datei dieselben Beispiele aus.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

def median(values: list[float]) -> float:
    """Return the middle value; for an even count, the mean of the two middle values.

    >>> median([3, 1, 2])
    2
    >>> median([4, 1, 3, 2])
    2.5
    >>> median([])
    Traceback (most recent call last):
    ...
    ValueError: median of an empty list
    """
    if not values:
        raise ValueError("median of an empty list")
    ordered = sorted(values)
    middle = len(ordered) // 2
    if len(ordered) % 2 == 1:
        return ordered[middle]
    return (ordered[middle - 1] + ordered[middle]) / 2


if __name__ == "__main__":
    print(median([3, 1, 2]), median([4, 1, 3, 2]))
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

def median(values: list[float]) -> float:
    """Return the middle value; for an even count, the mean of the two middle values.

    >>> median([3, 1, 2])
    2
    """
    # Add two more examples to the docstring above:
    # - an even number of values, such as [4, 1, 3, 2]
    # - an empty list, which raises ValueError: median of an empty list
    if not values:
        raise ValueError("median of an empty list")
    ordered = sorted(values)
    middle = len(ordered) // 2
    if len(ordered) % 2 == 1:
        return ordered[middle]
    return (ordered[middle - 1] + ordered[middle]) / 2


if __name__ == "__main__":
    print(median([3, 1, 2]), median([4, 1, 3, 2]))

test_main.py

import doctest

import main


def run_examples(implementation):
    parser = doctest.DocTestParser()
    test = parser.get_doctest(main.median.__doc__ or "", {"median": implementation}, "median", "main.py", 0)
    runner = doctest.DocTestRunner()
    return runner.run(test, out=lambda text: None)


def middle_only(values):
    ordered = sorted(values)
    return ordered[len(ordered) // 2]


def no_empty_check(values):
    ordered = sorted(values)
    middle = len(ordered) // 2
    if len(ordered) % 2 == 1:
        return ordered[middle]
    return (ordered[middle - 1] + ordered[middle]) / 2


def test_examples_pass():
    """Ihr Docstring hat mindestens 3 Beispiele, und sie bestehen"""
    failed, attempted = run_examples(main.median)
    assert attempted >= 3, f"der Docstring hat {attempted} Beispiele; schreiben Sie mindestens 3"
    assert failed == 0, f"{failed} Ihrer Beispiele scheitern am korrekten median"


def test_catches_even_bug():
    """Ihre Beispiele erkennen ein median, das gerade Anzahlen übergeht"""
    failed, _ = run_examples(middle_only)
    assert failed > 0, "alle Beispiele bestehen, obwohl median([4, 1, 3, 2]) 3 liefert; ergänzen Sie ein Beispiel mit gerader Länge"


def test_catches_empty_bug():
    """Ihre Beispiele erkennen ein median ohne Prüfung auf die leere Liste"""
    failed, _ = run_examples(no_empty_check)
    assert failed > 0, "alle Beispiele bestehen, obwohl median([]) IndexError auslöst; zeigen Sie den ValueError in einem Beispiel"

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 Verhalten pro Test

Ein gültiger Benutzername hat 3 bis 15 Zeichen: Buchstaben, Ziffern oder _, und beginnt mit einem Buchstaben. Ersetzen Sie test_everything durch mindestens fünf fokussierte Tests, jeder benannt nach dem einen Verhalten, das er prüft. Decken Sie beide Längengrenzen und die Längen knapp daneben ab, einen Namen mit Ziffer am Anfang und ein verbotenes Zeichen. Die Prüfungen lassen Ihre Tests gegen sechs fehlerhafte Versionen laufen, die je eine Regel falsch umsetzen.

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

    Eine Methode pro Regel, etwa def test_two_characters_is_too_short(self) -> None: mit einem einzigen assertFalse darin.

  2. Hinweis 2

    Grenzen kommen paarweise: "ada" (3) muss bestehen und "al" (2) scheitern; "a" * 15 muss bestehen und "a" * 16 scheitern.

  3. Hinweis 3

    Zwei weitere Tests: ein Name mit Ziffer am Anfang wie "1ada" und einer mit Bindestrich wie "ada-lovelace".

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import unittest


def is_valid_username(name: str) -> bool:
    """3 to 15 characters: letters, digits or _, starting with a letter."""
    if not 3 <= len(name) <= 15:
        return False
    if not name[0].isalpha():
        return False
    return all(ch.isalnum() or ch == "_" for ch in name)


class TestUsername(unittest.TestCase):
    def test_typical_name_is_valid(self) -> None:
        self.assertTrue(is_valid_username("ada_1815"))

    def test_three_characters_is_shortest_allowed(self) -> None:
        self.assertTrue(is_valid_username("ada"))

    def test_two_characters_is_too_short(self) -> None:
        self.assertFalse(is_valid_username("al"))

    def test_fifteen_characters_is_longest_allowed(self) -> None:
        self.assertTrue(is_valid_username("a" * 15))

    def test_sixteen_characters_is_too_long(self) -> None:
        self.assertFalse(is_valid_username("a" * 16))

    def test_must_start_with_a_letter(self) -> None:
        self.assertFalse(is_valid_username("1ada"))

    def test_hyphen_is_not_allowed(self) -> None:
        self.assertFalse(is_valid_username("ada-lovelace"))


if __name__ == "__main__":
    suite = unittest.TestLoader().loadTestsFromTestCase(TestUsername)
    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 is_valid_username(name: str) -> bool:
    """3 to 15 characters: letters, digits or _, starting with a letter."""
    if not 3 <= len(name) <= 15:
        return False
    if not name[0].isalpha():
        return False
    return all(ch.isalnum() or ch == "_" for ch in name)


class TestUsername(unittest.TestCase):
    # Split this into focused tests, one behaviour each, and add the
    # missing edge cases: both length limits and their neighbours,
    # a name starting with a digit, and a character that is not allowed.
    def test_everything(self) -> None:
        self.assertTrue(is_valid_username("ada_1815"))
        self.assertFalse(is_valid_username("al"))


if __name__ == "__main__":
    suite = unittest.TestLoader().loadTestsFromTestCase(TestUsername)
    unittest.TextTestRunner(verbosity=2).run(suite)

test_main.py

import unittest

import main


def run_against(implementation):
    saved = main.is_valid_username
    main.is_valid_username = implementation
    try:
        result = unittest.TestResult()
        unittest.TestLoader().loadTestsFromTestCase(main.TestUsername).run(result)
    finally:
        main.is_valid_username = saved
    return result


def rules(name, low=3, high=15, digit_start=False, hyphen=False):
    if not low <= len(name) <= high:
        return False
    if not (name[0].isalpha() or (digit_start and name[0].isdigit())):
        return False
    return all(ch.isalnum() or ch == "_" or (hyphen and ch == "-") for ch in name)


BROKEN = {
    "3 Zeichen abgelehnt werden": lambda name: rules(name, low=4),
    "2 Zeichen angenommen werden": lambda name: rules(name, low=2),
    "15 Zeichen abgelehnt werden": lambda name: rules(name, high=14),
    "16 Zeichen angenommen werden": lambda name: rules(name, high=16),
    "ein Name mit einer Ziffer beginnen darf": lambda name: rules(name, digit_start=True),
    "ein Bindestrich angenommen wird": lambda name: rules(name, hyphen=True),
}


def test_correct_code_passes():
    """Mindestens 5 Tests, und alle bestehen mit der korrekten Funktion"""
    result = run_against(main.is_valid_username)
    failing = [test.id() for test, _ in result.failures + result.errors]
    assert result.wasSuccessful(), f"diese Tests scheitern an korrektem Code: {failing}"
    assert result.testsRun >= 5, f"{result.testsRun} Tests liefen; schreiben Sie mindestens 5 fokussierte Tests"


def test_every_bug_is_caught():
    """Jeder von sechs Ein-Regel-Fehlern lässt einen Test scheitern"""
    missed = [bug for bug, broken in BROKEN.items() if run_against(broken).wasSuccessful()]
    if missed:
        assert False, f"kein Test scheitert, wenn {missed[0]}; ergänzen Sie diesen Randfall"


def test_tests_are_focused():
    """Ein Ein-Regel-Fehler lässt die übrigen Tests bestehen"""
    for bug, broken in BROKEN.items():
        result = run_against(broken)
        bad = len(result.failures) + len(result.errors)
        assert bad < result.testsRun, f"wenn {bug}, scheitert jeder Test; prüfen Sie ein Verhalten pro Test"

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

Kein Leerzeichen nach >>>

import doctest


def add(a, b):
    """Return a + b.

    >>>add(1, 2)
    3
    """
    return a + b


doctest.run_docstring_examples(add, globals(), name="add")

Was Python ausgibt

ValueError: line 3 of the docstring for add lacks blank after >>>: '>>>add(1, 2)'

Warum, und die Lösung

doctest erkennt ein Beispiel nur an >>> mit folgendem Leerzeichen, wie die Eingabeaufforderung einer interaktiven Sitzung. Ohne das Leerzeichen weigert es sich, den Docstring überhaupt zu lesen. Schreiben Sie >>> add(1, 2).

Ausgabe anders eingerückt als ihr Beispiel

import doctest


def add(a, b):
    """Return a + b.

      >>> add(1, 2)
    3
    """
    return a + b


doctest.run_docstring_examples(add, globals(), name="add")

Was Python ausgibt

ValueError: line 4 of the docstring for add has inconsistent leading whitespace: '3'

Warum, und die Lösung

Die erwartete Ausgabe muss in derselben Spalte beginnen wie das >>> ihres Beispiels. Hier ist das Beispiel zwei Leerzeichen weiter eingerückt als seine Ausgabe, also kann doctest nicht erkennen, wo die Ausgabe beginnt. Richten Sie beide am übrigen Docstring aus.

Einen Modulnamen statt des Moduls übergeben

import doctest


def add(a, b):
    """
    >>> add(1, 2)
    3
    """
    return a + b


doctest.testmod("main")

Was Python ausgibt

TypeError: testmod: module required; 'main'

Warum, und die Lösung

testmod(m) braucht ein Modulobjekt, nicht seinen Namen als String. Rufen Sie doctest.testmod() ohne Argumente auf, um das Modul zu testen, das Sie als Skript starten, oder importieren Sie das Modul zuerst und übergeben es: import shop, dann doctest.testmod(shop).

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

Beispiele, die sich selbst prüfen

doctest sucht in Docstrings nach Text, der wie eine interaktive Sitzung aussieht: eine Zeile mit >>> und darunter die erwartete Ausgabe. Es führt den Code aus und vergleicht die Ausgabe genau, Zeichen für Zeichen. Ein Wert erscheint als sein repr, ein String also mit Anführungszeichen; ausgegebener Text erscheint ohne. Die erwartete Ausgabe endet beim nächsten >>> oder bei einer Leerzeile. Für eine Ausnahme schreiben Sie Traceback (most recent call last): und dann die Zeile mit der Ausnahme; der Stack dazwischen wird ignoriert und darf ... sein.

Doctests ausführen

Üblich ist, ein Modul mit if __name__ == "__main__": import doctest; doctest.testmod() zu beenden. Als Skript gestartet, prüft das jeden Docstring des Moduls und gibt nichts aus, wenn alle Beispiele bestehen; ein Fehlschlag zeigt das Beispiel, das Erwartete und das Erhaltene. testmod() liefert TestResults(failed, attempted). Doctests eignen sich am besten als Dokumentation, die wahr bleibt; genaue Prüfungen vieler Fälle gehören in unittest-Tests.

Was man testet und wie viel pro Test

Fehler sammeln sich an den Rändern: eine leere Liste, ein einzelnes Element, null, negative Zahlen und beide Seiten jeder Grenze. Heißt eine Regel „ab 18“, testen Sie 17 und 18. Lassen Sie jeden Test ein Verhalten prüfen und benennen Sie ihn danach, etwa test_empty_list_raises. Ein Test endet beim ersten gescheiterten assert; ein Test mit fünf Regeln verdeckt also die anderen vier. Mit fokussierten Tests scheitert bei einem Fehler genau der Test, der ihn benennt.

Quellen

Zuletzt geprüft am 29. September 2026