0%

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_question

  • test_a_new_quiz_has_no_points

  • test_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 maakt total_points gelijk aan 5.

  • test_the_returned_list_is_a_copy: toevoegen aan wat questions() 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.