Testing and Refactoring Object-oriented Programs · practice
Test Construction and State Changes
Question and Quiz were gentle to test, because asking them something twice gives the same answer both times.
Attempt is different. It has a score that moves. Testing an
Test the starting state
Chapter 5 spent a whole chapter on the idea that a constructor’s job is to produce an object that is already valid. That claim is testable, and it is the first test to write:
def test_a_new_attempt_has_not_scored():
quiz = Quiz("Review")
attempt = Attempt(quiz, "Mina")
assert attempt.score == 0
This one looks almost too small to bother with. It is not. It is the test that fails the day someone gives _earned a different starting
Notice the arrangement: constructing the object is the act here. That is what makes it a construction test.
Test a change as before and after
For a method that changes state, the assertion is not about a value. It is about a difference.
def test_a_correct_answer_adds_the_points():
quiz = Quiz("Review")
question = Question("Which keyword starts a function?", "def", 2)
quiz.add(question)
attempt = Attempt(quiz, "Mina")
attempt.answer(question, "def")
assert attempt.score == 2
answer() returns nothing. There is no
Test that nothing happened
The other half of a state change is the case where the state should not move:
def test_a_wrong_answer_scores_nothing():
...
attempt.answer(question, "class")
assert attempt.score == 0
These tests feel redundant while you write them and earn their place later. “Adds the points when correct” and “adds nothing when wrong” are two different promises, and code can easily keep the first while breaking the second. A method that always adds the points passes the first test perfectly.
Why does test_a_wrong_answer_scores_nothing matter, when there is already a test for the correct answer?
When the sequence is the behavior
Last lesson said one act per test, with one honest
def test_answering_several_questions_accumulates():
...
attempt.answer(first, "def")
attempt.answer(second, "class")
assert attempt.score == 5
Two calls, one act. The behavior under test is not “answering a question”, it is “answering questions builds up a total”, and you cannot express that with a single call. The name says as much, so a reader is not surprised.
The line to hold is whether the calls form one idea. Two calls that accumulate are one idea. A correct answer followed by a wrong one can also form one useful sequence: it checks that a wrong answer preserves points already earned. Split a test when it combines independent behaviors, not merely because it contains two calls.
Task
Write test_attempt.py, covering how an Attempt starts and how it changes.
Import Attempt, Question, and Quiz from quizapp.models, then write four tests:
test_a_new_attempt_has_not_scored: a fresh attempt scores 0.test_a_correct_answer_adds_the_points: answering a 2-point question correctly makes the score 2.test_a_wrong_answer_scores_nothing: answering the same question wrongly leaves the score at 0.test_answering_several_questions_accumulates: answering a 2-point and a 3-point question correctly makes the score 5.
Every question you answer has to be in the quiz first. Assert against score, never against _earned.