0%

Hoofdstuk 13 · oefening

Objectgerichte programma’s testen en refactoren

Test publiek gedrag

Python-basis I sloot een hoofdstuk af met losse asserties onder de code die ze controleerden:

assert double(4) == 8

Dat werkt en het blijft de kern van wat je gaat doen. Wat ontbreekt is een eigen plek. Asserties onder de code worden elke keer uitgevoerd als het programma draait, verdwijnen bij het opruimen en stoppen bij de eerste die faalt.

Een test is dezelfde assertie met een naam en een eigen plek.

Een test is een functie waarvan de naam begint met test_

def test_a_correct_response_scores():
    question = Question("Which keyword starts a function?", "def", 2)

    assert question.is_correct("def")

Dat is de hele vorm. Geen nieuwe syntaxis: een functiedefinitie en een assertie, die je allebei al kent.

Nieuw is de conventie. De naam doet er niet alleen voor jou toe. Python Land verzamelt functies in je testbestanden waarvan de naam met test_ begint en voert elke functie afzonderlijk uit, zodat een fout in de eerste de rest niet verbergt.

Tests staan in eigen bestanden en ook die namen volgen een conventie. Een bestand telt als testbestand als het tests.py heet, met test_ begint of op _test.py eindigt. Dit hoofdstuk gebruikt één bestand per idee: test_question.py bevat de tests voor Question.

Run tests is een aparte knop

De editor heeft nu twee acties.

Run voert je programma uit zoals iemand die het gebruikt dat zou doen.

Run tests importeert je testmodules en hun afhankelijkheden, voert daarna elke test_-functie uit en rapporteert elke test. Het start de applicatie niet via het bewaakte startpunt, maar onbewaakte code op het hoogste niveau zou tijdens die imports wel worden uitgevoerd. Het voltooit de les ook niet. Submit doet dat nog steeds en Submit heeft nog steeds eigen controles die je niet kunt zien.

Run tests is dus echt van jou. Het bestaat om je te vertellen of de code doet wat jij denkt, voordat iemand anders dat vraagt.

Test het gedrag, niet de interne werking

Dit is het onderscheid waarop de rest van het hoofdstuk rust.

Question belooft iets: geef een antwoord en het vertelt of dat antwoord juist is. Die belofte is het publieke gedrag en dat is waar andere code van afhangt.

Onderliggend roept is_correct toevallig .strip() en .lower() aan. Dat is interne werking. Die kan morgen worden herschreven om anders te vergelijken. Zolang de belofte blijft gelden, merkt niets dat Question gebruikt dat.

Een test die de belofte controleert:

def test_capitalization_does_not_matter():
    question = Question("Which keyword starts a function?", "def", 2)

    assert question.is_correct("DEF")

Een test die de interne werking controleert:

def test_is_correct_calls_lower():
    ...

De eerste test overleeft een herschrijving. De tweede gaat stuk bij een herschrijving die niets verandert wat iemand kan waarnemen. Een test die faalt wanneer er niets mis is, is erger dan geen test, omdat je leert die te negeren.

Hoofdstuk 5 trok deze grens al, tussen de attributen die een klasse openbaar maakt en de attributen die die voor zichzelf houdt. Bij tests begint die grens voordeel op te leveren.

Attempt bewaart een lopend totaal in self._earned en maakt dat beschikbaar via een property score. Welke test controleert publiek gedrag?

Je eerste test schrijven

Alles in dit hoofdstuk bouwt voort op één quizapplicatie die al geschreven is en werkt. Die gebruikt bewust de eerdere stapsgewijze Quiz, die leeg begint en vragen ontvangt via add(), omdat de toestandswijzigingen nuttig testmateriaal zijn. Het afsluitende project van hoofdstuk 12 gebruikte een andere afspraak: de uitvoerbare Quiz ontving bij het aanmaken een volledige vragenlijst en weigerde een lege lijst. Behandel de startversie van dit hoofdstuk als een verwante applicatie met een expliciet andere grens, niet als dezelfde klasse die ongemerkt de belofte verandert.

Je taak dit hoofdstuk is niet de applicatie bouwen. Het is uitzoeken wat die echt belooft, die beloften als tests opschrijven en ze daarna gebruiken om de code veilig te veranderen.

Begin met het kleinste object erin.

Question ontvangt een vraagtekst, een antwoord en een aantal punten. De enige methode, is_correct, vergelijkt een gegeven antwoord met het juiste antwoord en negeert daarbij spaties aan de uiteinden en hoofdlettergebruik.

Drie beloften zijn het vastleggen waard: een overeenkomend antwoord is juist, een ander antwoord niet en spaties noch hoofdlettergebruik veranderen het oordeel.

Opdracht

Schrijf test_question.py, het eerste testbestand voor de quizapplicatie.

Importeer Question uit quizapp.models en schrijf daarna drie testfuncties:

  • test_a_matching_response_is_correct: een antwoord gelijk aan het juiste antwoord is juist.

  • test_a_different_response_is_not_correct: een ander antwoord is dat niet.

  • test_spacing_and_capitalization_do_not_matter: een antwoord zoals " DEF " is nog steeds juist.

Elke test maakt een eigen Question en controleert is_correct met een assertie. Controleer alleen wat Question belooft: roep geen .strip() of .lower() aan in de test en benader geen attribuut dat met een underscore begint. Commentaar dat die keuze uitlegt is prima.

Gebruik Run tests om alle drie te zien slagen voordat je op Submit klikt.