Aufwärmen · Aufgabe 1 von 7
// 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.
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
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()Ü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.())Ü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())Ü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.
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())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.pyAusgabe
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
Hinweis 1
Ein Beispiel ist eine Zeile >>> median([4, 1, 3, 2]) und darunter, gleich eingerückt, die genaue Ausgabe: 2.5.
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.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
Eine Methode pro Regel, etwa def test_two_characters_is_too_short(self) -> None: mit einem einzigen assertFalse darin.
Hinweis 2
Grenzen kommen paarweise: "ada" (3) muss bestehen und "al" (2) scheitern; "a" * 15 muss bestehen und "a" * 16 scheitern.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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.