Objectgerichte programma’s testen en refactoren · oefening
Arrange, act, assert
Je drie tests voor Question hadden allemaal dezelfde vorm, of je dat nu merkte tijdens het schrijven of niet:
def test_a_matching_response_is_correct():
question = Question("Which keyword starts a function?", "def", 2)
assert question.is_correct("def")
Bouw iets. Doe er iets mee. Controleer wat er gebeurde.
Die vorm heet arrange, act, assert: voorbereiden, uitvoeren, controleren. De naam is nuttig, omdat je zodra je het patroon ziet in één oogopslag kunt bepalen of een test één ding doet of vier.
De drie fasen
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 brengt de omgeving in de toestand waarover de test gaat. Act is het ene ding dat wordt getest. Assert controleert het resultaat.
De lege regels doen hier echt werk. Een lezer die wil weten waar deze test over gaat leest de middelste regel en stopt.
Eén gedrag per test
De vorm geeft je een regel die makkelijker te volgen is dan ‘houd tests klein’: een test heeft één act.
Als je merkt dat je een tweede act schrijft, heb je een tweede test gevonden:
def test_answering():
...
attempt.answer(first, "def")
assert attempt.score == 2
attempt.answer(second, "wrong") # a second act
assert attempt.score == 2
Splits die en elke helft krijgt een naam die zegt wat die controleert. Nog beter: als er één stukgaat, draait de andere nog steeds. De twee resultaten vertellen je meer dan één mislukking zou doen.
Er is één terechte uitzondering. Soms is het gedrag echt een reeks: twee vragen achter elkaar beantwoorden moet optellen. Dan is de reeks de act en zegt de testnaam dat.
De naam is documentatie
test_1 vertelt een toekomstige lezer niets. Als die faalt, moet de lezer de inhoud lezen om te ontdekken wat er stukging.
Een goede naam is een zin over gedrag, met test_ ervoor:
test_total_points_sums_every_questiontest_a_new_quiz_has_no_pointstest_a_wrong_answer_does_not_change_the_score
Lang is prima. De naam verschijnt in het testrapport naast de foutdetails en hoort dus te vertellen wat het programma had moeten doen.
Een test controleert of een quiz de punten optelt en controleert daarna afzonderlijk of een vraag hoofdlettergebruik negeert. Waarom splits je die controles in twee tests?
Testen wat een methode belooft niet te doen
Quiz.questions() geeft een lijst terug. Kijk hoe:
def questions(self):
return list(self._questions)
Die list(...) is een belofte: wat je terugkrijgt is van jou en erin rommelen raakt de binnenkant van de quiz niet. Hoofdstuk 6 noemde dit je eigen toestand blijven beheren.
Zo’n belofte verdient een test, omdat het precies het soort ding is dat een latere ‘vereenvoudiging’ verwijdert:
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
Verwijder de list(...) en die test faalt meteen. Daarvoor dient een test.
Opdracht
Schrijf test_quiz.py voor de beloften van Quiz.
Importeer Question en Quiz uit quizapp.models en schrijf daarna drie tests:
test_a_new_quiz_has_no_points: een quiz waaraan niets is toegevoegd is 0 punten waard.test_total_points_sums_every_question: vragen van 2 en 3 punten toevoegen maakttotal_pointsgelijk aan 5.test_the_returned_list_is_a_copy: toevoegen aan watquestions()teruggeeft verandert de quiz niet.
Geef elke test de drie fasen, gescheiden door lege regels: bereid de quiz voor, voer één act uit en controleer daarna met asserties. Houd één act per test.