Objectgerichte programma’s testen en refactoren · oefening
Test verwachte excepties
Hoofdstuk 12 stelde dat weigeren iets te doen deel uitmaakt van wat een methode belooft, geen toevalligheid. Question weigert te bestaan met minder dan één punt. Attempt weigert een vraag te beoordelen die niet in de quiz staat.
Die weigeringen zijn gedrag. Ze verdienen net zo goed tests als de geslaagde gevallen en hebben een andere vorm nodig, omdat je controleert dat de code stopte.
Waarom de voor de hand liggende versie niet werkt
def test_a_question_needs_points():
Question("Which keyword?", "def", 0)
assert ???
Er is niets om met een assertie te controleren. Of de aanroep gooide een exceptie op en de test bereikte de assertie nooit, of die deed dat niet, waarna de belofte werd gebroken en de test toch slaagt.
Je zou try en except kunnen gebruiken en dat werkt, maar lees wat ervoor nodig is:
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")
Vijf regels afhandelingscode rond één regel gedrag, en de return in het midden is het deel dat ‘geslaagd’ zegt. Dit komt vaak genoeg voor om een hulpmiddel te verdienen.
raises
Python Land geeft je er één:
from pythonland_test import raises
def test_a_question_needs_points():
with raises(ValueError):
Question("Which keyword starts a function?", "def", 0)
Lees het als een zin: dit blok gooit ValueError op. Als dat gebeurt, slaagt de test. Als het blok zonder exceptie tot het einde draait, faalt de test. Als het iets heel anders opgooit, gaat die fout rechtstreeks door, omdat een onverwachte fout een ander probleem is dan een onvervulde verwachting en niet als zodanig vermomd moet worden.
raises komt uit pythonland_test, een module die dit platform biedt. Je vindt die niet in een Pythoninstallatie op je eigen computer. Waarvoor het staat is wel universeel: elk serieus Pythontesthulpmiddel heeft dit, meestal met bijna dezelfde schrijfwijze.
Kort over with
Die with is nieuw.
Een with-instructie voert een blok uit onder toezicht van iets. Het object na with krijgt te horen wanneer het blok begint en eindigt, ook als het eindigt door een fout, en kan beslissen wat daarmee gebeurt. raises gebruikt het einde: het bekijkt hoe het blok eindigde en beoordeelt dat.
Je komt with later weer tegen bij bestanden en verbindingen, waar het toezicht over achteraf opruimen gaat. Houd hier alleen de vorm vast: with iets, daarna een ingesprongen blok, en dat iets heeft invloed op hoe het blok afloopt.
Controleren welke fout en wat die zegt
De gegeven quizapp/errors.py geeft UnknownQuestionError een attribuut prompt, zodat een aanroeper het probleem goed kan melden. Een test kan dat bekijken:
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"
Net als except accepteert raises het genoemde exceptietype en de subklassen ervan. Gebruik een specifieke subklasse als dat onderscheid deel is van de belofte.
as caught geeft je een verwijzing en caught.value is het exceptieobject zelf nadat het blok is afgerond. Vanaf daar is het een gewoon object: lees de attributen of geef het aan str() voor het bericht.
Dat is de extra regel waard. ‘Het gooide iets op’ is een veel zwakkere belofte dan ‘het gooide de fout op die de betreffende vraag noemt’, en hoofdstuk 12 bouwde bewust de tweede.
UnknownQuestionError is een subklasse van QuizError. Een test zet with raises(QuizError): rond code die UnknownQuestionError opgooit. Wat gebeurt er?
Welke weigeringen je vastlegt
De quizapplicatie doet er twee en ze zijn van verschillende soorten.
Question weigert ongeldig aanmaken met een gewone ValueError. Het is een ongeldig argument, verder niets, en Python heeft daar al een woord voor.
Attempt.answer weigert een vraag van buiten de quiz met UnknownQuestionError, dat de applicatie zelf definieerde omdat een aanroeper redelijkerwijs precies dit kan willen opvangen en melden. Test beide en controleer bij de tweede of de vraagtekst wordt meegedragen.
Opdracht
Schrijf test_rules.py voor de twee weigeringen van de quizapplicatie.
Importeer raises uit pythonland_test, UnknownQuestionError uit quizapp.errors en wat je nodig hebt uit quizapp.models. Schrijf drie tests:
test_a_question_needs_at_least_one_point: eenQuestionvan 0 punten aanmaken gooitValueErrorop.test_answering_a_foreign_question_is_refused: een vraag beantwoorden die nooit aan de quiz is toegevoegd gooitUnknownQuestionErrorop.test_the_refusal_names_the_question: diezelfde fout bevat de vraagtekst van de buitenstaander in het attribuutprompt.
Beperk elk blok with raises(...) tot de ene aanroep die moet worden geweigerd. Alles wat je eerst voorbereidt hoort boven het blok, waar een fout eerlijk wordt gemeld in plaats van als de verwachte fout te tellen.