Objektorientierte Programme testen und umstrukturieren · Übung
Erwartete Ausnahmen testen
Kapitel 12 erklärte, dass eine Ablehnung Teil der Zusage einer Methode ist und kein Zufall. Question lehnt es ab, mit weniger als einem Punkt zu existieren. Attempt lehnt es ab, eine Frage zu bewerten, die nicht zu seinem Quiz gehört.
Diese Ablehnungen sind Verhalten. Sie verdienen ebenso Tests wie erfolgreiche Abläufe. Dafür brauchen sie eine andere Form, denn du prüfst, dass der Code angehalten hat.
Warum die naheliegende Version nicht funktioniert
def test_a_question_needs_points():
Question("Which keyword?", "def", 0)
assert ???
Es gibt nichts, worauf sich eine Zusicherung beziehen könnte. Entweder hat der Aufruf eine Ausnahme ausgelöst, dann erreicht der Test die Zusicherung nie. Oder er hat keine ausgelöst, dann wurde die Zusage verletzt, und der Test besteht trotzdem.
Du könntest zu try und except greifen. Das funktioniert, aber sieh dir den Aufwand an:
def test_a_question_needs_points():
try:
Question("Which keyword?", "def", 0)
except ValueError:
return
raise AssertionError("a question with 0 points should have been refused")
Fünf Zeilen Verwaltungsarbeit um eine Zeile Verhalten, und das return mittendrin sagt „Bestanden“. Das kommt häufig genug vor, um ein Werkzeug zu verdienen.
raises
Python Land stellt dir eines bereit:
from pythonland_test import raises
def test_a_question_needs_points():
with raises(ValueError):
Question("Which keyword starts a function?", "def", 0)
Lies es als Satz: Dieser Block löst ValueError aus. Tut er das, besteht der Test. Läuft der Block ohne Ausnahme bis zum Ende, schlägt der Test fehl. Löst er etwas völlig anderes aus, wird dieser Fehler direkt weitergegeben: Ein unerwarteter Fehler ist ein anderes Problem als eine nicht erfüllte Erwartung und sollte nicht als solche getarnt werden.
raises stammt aus pythonland_test, einem Modul, das diese Plattform bereitstellt. Du findest es nicht in einer Python-Installation auf deinem eigenen Rechner. Die Idee dahinter ist allerdings allgemein: Jedes ernsthafte Python-Testwerkzeug hat etwas dafür, meistens fast genauso geschrieben.
Kurz zu with
Dieses with ist neu.
Eine with-Anweisung führt einen Block unter der Aufsicht eines Objekts aus. Das Objekt hinter with wird benachrichtigt, wenn der Block beginnt und endet, auch wenn er wegen eines Fehlers endet. Es kann entscheiden, was dabei zu tun ist. raises nutzt das Ende: Es betrachtet, wie der Block abgeschlossen wurde, und beurteilt das Ergebnis.
Du wirst with bei Dateien und Verbindungen wiedersehen. Dort dient die Aufsicht dem anschließenden Aufräumen. Hier musst du dir nur die Form merken: with, gefolgt von etwas, und dann ein eingerückter Block. Dieses Etwas kann mitbestimmen, wie der Block endet.
Prüfen, welcher Fehler auftritt und was er sagt
Die vorgegebene quizapp/errors.py gibt UnknownQuestionError ein Attribut prompt, damit ein Aufrufer das Problem ordentlich melden kann. Ein Test kann es betrachten:
def test_answering_a_foreign_question_is_refused():
quiz = Quiz("Review")
stray = Question("Not in this quiz", "nope", 1)
attempt = Attempt(quiz, "Mina")
with raises(UnknownQuestionError) as caught:
attempt.answer(stray, "nope")
assert caught.value.prompt == "Not in this quiz"
Wie except akzeptiert raises den angegebenen Ausnahmetyp und dessen Unterklassen. Verwende eine spezifische Unterklasse, wenn diese Unterscheidung zur Zusage gehört.
as caught gibt dir einen Zugriffspunkt, und caught.value ist nach Abschluss des Blocks das Ausnahmeobjekt selbst. Von dort aus ist es ein gewöhnliches Objekt: Lies seine Attribute oder übergib es an str(), um die Meldung zu erhalten.
Das ist die zusätzliche Zeile wert. „Es hat irgendetwas ausgelöst“ ist eine viel schwächere Zusage als „Es hat den Fehler ausgelöst, der die problematische Frage nennt“. Kapitel 12 hat die zweite bewusst aufgebaut.
UnknownQuestionError ist eine Unterklasse von QuizError. Ein Test umschließt Code, der UnknownQuestionError auslöst, mit with raises(QuizError):. Was passiert?
Welche Ablehnungen festgehalten werden sollten
Die Quizanwendung hat zwei, und sie sind unterschiedlicher Art.
Question lehnt eine ungültige Konstruktion mit einem gewöhnlichen ValueError ab. Es ist ein ungültiges Argument, mehr nicht. Dafür hat Python bereits einen Begriff.
Attempt.answer lehnt eine Frage außerhalb seines Quiz mit UnknownQuestionError ab. Die Anwendung hat diesen Typ selbst definiert, weil ein Aufrufer vernünftigerweise genau diesen Fall abfangen und melden möchte. Teste beide und prüfe beim zweiten, dass er den Fragetext mitträgt.
Aufgabe
Schreibe test_rules.py und decke die beiden Ablehnungen der Quizanwendung ab.
Importiere raises aus pythonland_test, UnknownQuestionError aus quizapp.errors und das Benötigte aus quizapp.models. Schreibe drei Tests:
test_a_question_needs_at_least_one_point: Die Konstruktion einesQuestionmit 0 Punkten löstValueErroraus.test_answering_a_foreign_question_is_refused: Eine Frage zu beantworten, die dem Quiz nie hinzugefügt wurde, löstUnknownQuestionErroraus.test_the_refusal_names_the_question: Derselbe Fehler trägt den Fragetext der fremden Frage in seinem Attributprompt.
Beschränke jeden Block with raises(...) auf den einzelnen Aufruf, der abgelehnt werden soll. Alles, was du vorher vorbereitest, gehört oberhalb des Blocks. Dort würde ein Fehler ehrlich gemeldet, statt als erwartete Ausnahme zu zählen.