Objektorientierte Programme testen und umstrukturieren · Übung
Vorbereiten, handeln, prüfen
Deine drei Tests für Question hatten alle dieselbe Form, ob dir das beim Schreiben aufgefallen ist oder nicht:
def test_a_matching_response_is_correct():
question = Question("Which keyword starts a function?", "def", 2)
assert question.is_correct("def")
Etwas aufbauen. Etwas damit tun. Prüfen, was passiert ist.
Diese Form heißt Arrange, Act, Assert, also vorbereiten, handeln, prüfen. Der Name lohnt sich: Sobald du das Muster erkennst, siehst du auf einen Blick, ob ein Test eine Sache tut oder vier.
Die drei Phasen
def test_answering_correctly_adds_the_points():
quiz = Quiz("Review") # arrange
question = Question("Which keyword?", "def", 2)
quiz.add(question)
attempt = Attempt(quiz, "Mina")
attempt.answer(question, "def") # act
assert attempt.score == 2 # assert
Arrange versetzt die Umgebung in den Zustand, um den es im Test geht. Act ist die einzelne Handlung, die getestet wird. Assert prüft das Ergebnis.
Die Leerzeilen erfüllen hier eine echte Aufgabe. Wer wissen möchte, worum es in diesem Test geht, liest die mittlere Zeile und weiß Bescheid.
Ein Verhalten pro Test
Aus der Form ergibt sich eine Regel, die leichter einzuhalten ist als „Halte Tests klein“: Ein Test hat eine Handlung.
Wenn du bemerkst, dass du eine zweite Handlung schreibst, hast du einen zweiten Test gefunden:
def test_answering():
...
attempt.answer(first, "def")
assert attempt.score == 2
attempt.answer(second, "wrong") # a second act
assert attempt.score == 2
Teile das auf, und jede Hälfte bekommt einen Namen, der ihre Prüfung beschreibt. Noch besser: Wenn eine fehlschlägt, läuft die andere trotzdem. Die beiden Ergebnisse sagen dir mehr als ein einzelner Fehlschlag.
Es gibt eine begründete Ausnahme. Manchmal ist das Verhalten eine Abfolge: Zwei Fragen hintereinander zu beantworten soll Punkte ansammeln. Dann ist die Abfolge die Handlung, und der Testname sagt das.
Der Name ist Dokumentation
test_1 sagt späteren Lesenden nichts. Wenn der Test fehlschlägt, müssen sie den Rumpf lesen, um herauszufinden, was nicht funktioniert.
Ein guter Name ist ein Satz über Verhalten mit test_ davor:
test_total_points_sums_every_questiontest_a_new_quiz_has_no_pointstest_a_wrong_answer_does_not_change_the_score
Lange Namen sind in Ordnung. Der Name erscheint im Testbericht neben den Fehlerdetails und sollte dir sagen, was das Programm tun sollte.
Ein Test prüft, dass ein Quiz seine Punkte summiert, und anschließend unabhängig davon, dass eine Frage Groß- und Kleinschreibung ignoriert. Warum sollten diese Prüfungen in zwei Tests aufgeteilt werden?
Testen, was eine Methode ausdrücklich nicht tun soll
Quiz.questions() gibt eine Liste zurück. Schau dir an, wie:
def questions(self):
return list(self._questions)
Dieses list(...) ist eine Zusage: Was du zurückbekommst, gehört dir, und Änderungen daran reichen nicht in das Quiz hinein. Kapitel 6 nannte das, die Kontrolle über den eigenen Zustand zu behalten.
Eine solche Zusage verdient einen Test, denn genau solche Dinge entfernt eine spätere „Vereinfachung“:
def test_the_returned_list_is_a_copy():
quiz = Quiz("Review")
quiz.add(Question("Which keyword?", "def", 2))
quiz.questions().append("not a question")
assert len(quiz.questions()) == 1
Entferne list(...), und der Test schlägt sofort fehl. Genau dafür ist ein Test da.
Aufgabe
Schreibe test_quiz.py und decke die Zusagen von Quiz ab.
Importiere Question und Quiz aus quizapp.models und schreibe dann drei Tests:
test_a_new_quiz_has_no_points: Ein Quiz, dem noch nichts hinzugefügt wurde, ist 0 Punkte wert.test_total_points_sums_every_question: Werden Fragen mit 2 und 3 Punkten hinzugefügt, beträgttotal_points5.test_the_returned_list_is_a_copy: An die Rückgabe vonquestions()anzuhängen verändert das Quiz nicht.
Gib jedem Test die drei Phasen, durch Leerzeilen getrennt: Bereite das Quiz vor, führe eine Handlung aus und prüfe dann. Bleibe bei einer Handlung pro Test.