0%

Objektorientierte Programme testen und umstrukturieren · Übung

Konstruktion und Zustandsänderungen testen

Question und Quiz ließen sich angenehm testen, weil zweimal dieselbe Anfrage jeweils dieselbe Antwort liefert.

Attempt ist anders. Sein Punktestand verändert sich. Ein veränderliches Objekt zu testen bedeutet, zwei getrennte Dinge zu prüfen: wie es bei seiner Entstehung aussieht und was jede Methode damit macht.

Den Anfangszustand testen

Kapitel 5 widmete sich der Idee, dass ein Konstruktor ein bereits gültiges Objekt erzeugen soll. Diese Aussage lässt sich testen, und genau diesen Test schreibst du zuerst:

def test_a_new_attempt_has_not_scored():
    quiz = Quiz("Review")

    attempt = Attempt(quiz, "Mina")

    assert attempt.score == 0

Er wirkt fast zu klein, um sich die Mühe zu machen. Das ist er nicht. Er schlägt an dem Tag fehl, an dem jemand _earned einen anderen Anfangswert gibt oder ganz vergisst, es zu setzen. Und er schlägt sofort fehl statt erst drei Methoden später.

Beachte die Vorbereitung: Das Objekt zu konstruieren ist hier die Handlung. Genau das macht ihn zu einem Konstruktionstest.

Eine Änderung als Vorher und Nachher testen

Bei einer Methode, die Zustand verändert, geht es in der Zusicherung nicht um einen Wert, sondern um einen Unterschied.

def test_a_correct_answer_adds_the_points():
    quiz = Quiz("Review")
    question = Question("Which keyword starts a function?", "def", 2)
    quiz.add(question)
    attempt = Attempt(quiz, "Mina")

    attempt.answer(question, "def")

    assert attempt.score == 2

answer() gibt nichts zurück. Es gibt keinen Rückgabewert zu prüfen, und das ist normal für eine Methode, deren Aufgabe darin besteht, das Objekt zu ändern. Stattdessen prüfst du das Objekt danach.

Testen, dass nichts passiert ist

Die andere Hälfte einer Zustandsänderung ist der Fall, in dem sich der Zustand nicht ändern soll:

def test_a_wrong_answer_scores_nothing():
    ...
    attempt.answer(question, "class")

    assert attempt.score == 0

Diese Tests fühlen sich beim Schreiben überflüssig an und rechtfertigen ihren Platz später. „Addiert die Punkte bei einer richtigen Antwort“ und „Addiert nichts bei einer falschen Antwort“ sind zwei verschiedene Zusagen. Code kann leicht die erste einhalten und die zweite verletzen. Eine Methode, die immer die Punkte addiert, besteht den ersten Test problemlos.

Warum ist test_a_wrong_answer_scores_nothing wichtig, wenn es schon einen Test für die richtige Antwort gibt?

Wenn die Abfolge das Verhalten ist

Die letzte Lektion sagte: eine Handlung pro Test, mit einer begründeten Ausnahme. Hier ist sie:

def test_answering_several_questions_accumulates():
    ...
    attempt.answer(first, "def")
    attempt.answer(second, "class")

    assert attempt.score == 5

Zwei Aufrufe, eine Handlung. Getestet wird nicht „Eine Frage beantworten“, sondern „Fragen zu beantworten baut einen Gesamtpunktestand auf“. Das lässt sich nicht mit einem einzelnen Aufruf ausdrücken. Der Name sagt genau das, sodass Lesende nicht überrascht sind.

Entscheidend ist, ob die Aufrufe eine gemeinsame Idee bilden. Zwei Aufrufe, die Punkte ansammeln, sind eine Idee. Eine richtige Antwort gefolgt von einer falschen kann ebenfalls eine nützliche Abfolge bilden: Sie prüft, dass eine falsche Antwort den bereits erreichten Punktestand unverändert lässt. Teile einen Test auf, wenn er unabhängige Verhaltensweisen kombiniert, nicht bloß, weil er zwei Aufrufe enthält.

Aufgabe

Schreibe test_attempt.py und decke ab, wie ein Attempt beginnt und sich verändert.

Importiere Attempt, Question und Quiz aus quizapp.models und schreibe dann vier Tests:

  • test_a_new_attempt_has_not_scored: Ein neuer Versuch hat 0 Punkte.

  • test_a_correct_answer_adds_the_points: Eine Frage mit 2 Punkten richtig zu beantworten setzt den Punktestand auf 2.

  • test_a_wrong_answer_scores_nothing: Dieselbe Frage falsch zu beantworten lässt den Punktestand bei 0.

  • test_answering_several_questions_accumulates: Eine Frage mit 2 Punkten und eine mit 3 Punkten richtig zu beantworten ergibt 5 Punkte.

Jede Frage, die du beantwortest, muss zuerst im Quiz sein. Prüfe mit Zusicherungen score, niemals _earned.