Testing and Refactoring Object-oriented Programs · practice
Arrange, Act, Assert
Your three tests for Question all had the same shape, whether or not you noticed while writing them:
def test_a_matching_response_is_correct():
question = Question("Which keyword starts a function?", "def", 2)
assert question.is_correct("def")
Build something. Do something to it. Check what happened.
That shape has a name, arrange, act, assert, and it is worth naming because once you see it you can tell at a glance whether a test is doing one thing or four.
The three phases
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 puts the world into the state the test is about. Act is the single thing being tested. Assert checks the result.
The blank lines are doing real work here. A reader who wants to know what this test is about reads the middle line and stops.
One behavior per test
The shape gives you a rule that is easier to follow than “keep tests small”: a test has one act.
When you catch yourself writing a second act, you have found a second test:
def test_answering():
...
attempt.answer(first, "def")
assert attempt.score == 2
attempt.answer(second, "wrong") # a second act
assert attempt.score == 2
Split that and each half gets a name that says what it checks. Better still, when one breaks, the other still runs, and the pair of results tells you more than a single failure would.
There is one honest
The name is documentation
test_1 tells a future reader nothing. When it fails, they have to read the body to find out what broke.
A good name is a sentence about behavior, with test_ in front of it:
test_total_points_sums_every_questiontest_a_new_quiz_has_no_pointstest_a_wrong_answer_does_not_change_the_score
Long is fine. The name appears in the test report alongside failure details, so it should tell you what the program was supposed to do.
A test checks that a quiz totals its points, then separately checks that a question ignores capitalization. Why split those checks into two tests?
Testing what a method promises not to do
Quiz.questions() hands back a
def questions(self):
return list(self._questions)
That list(...) is a promise: what you get back is yours, and scribbling on it will not reach inside the quiz. Chapter 6 called this keeping ownership of your own state.
A promise like that deserves a test, because it is exactly the kind of thing a later “simplification” removes:
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
Delete the list(...) and that test fails immediately. That is what a test is for.
Task
Write test_quiz.py, covering what Quiz promises.
Import Question and Quiz from quizapp.models, then write three tests:
test_a_new_quiz_has_no_points: a quiz with nothing added is worth 0 points.test_total_points_sums_every_question: adding questions worth 2 and 3 makestotal_points5.test_the_returned_list_is_a_copy: appending to whateverquestions()returns does not change the quiz.
Give every test the three phases, separated by blank lines: arrange the quiz, act once, then assert. Keep one act per test.