Testing and Refactoring Object-oriented Programs · practice
Test Collaborating Objects
The tests so far each held one report.py does not work that way:
def report_lines(attempt):
fraction = percentage(attempt.score, attempt.quiz.total_points)
return [
f"{attempt.learner}: {attempt.score} of {attempt.quiz.total_points}",
f"Grade: {grade_letter(fraction)}",
]
One scoring. Chapter 6 built these collaborations deliberately. Now you have to test them.
Arrange the whole scene
The straightforward way is to build the real thing:
def test_the_report_names_the_learner_and_the_score():
quiz = Quiz("Review")
question = Question("Which keyword starts a function?", "def", 2)
quiz.add(question)
attempt = Attempt(quiz, "Mina")
attempt.answer(question, "def")
lines = report_lines(attempt)
assert lines[0] == "Mina: 2 of 2"
Five lines of arranging for one line of acting. That ratio is worth noticing, and Lesson 8 comes back to what it is telling you. For now, it is simply the price of testing a function that works with a small graph of objects.
The upside is real: nothing here is pretending. If Attempt and Quiz stop agreeing with each other, this test finds out, and a test of either one alone would not.
Assert on the result, not on the collaborators
report_lines returns a
It is tempting to reach further and assert that grade_letter was called, or that it was called with 1.0. Resist it. Those are steps, not results. Rewriting report_lines to compute the letter differently while producing the same lines would break such a test while breaking nothing real.
Check the strings. If the strings are right, the collaboration worked.
A stand-in, when the real thing is in the way
Sometimes you want to test how a function handles a
class FakeQuiz:
def __init__(self, total_points):
self.total_points = total_points
class FakeAttempt:
def __init__(self, learner, score, total_points):
self.learner = learner
self.score = score
self.quiz = FakeQuiz(total_points)
report_lines never asks what class it was handed. It asks for .learner, .score, and .quiz.total_points, and a stand-in that has those works perfectly.
That buys you exactness. Want the report for a learner who scored 4 out of 5? Say so directly, instead of building a quiz whose questions happen to add up that way and answering enough of them.
It costs you something too: a stand-in is a claim about how the real object behaves, and it will happily go on being wrong after the real one changes. Use real objects by default. Reach for a stand-in when arranging the real thing starts to obscure what the test is about.
report_lines is given a FakeAttempt with learner, score, and quiz.total_points. Why does it work?
What the report promises
headline(quiz) returns one string: the title, then the total points in brackets.
report_lines(attempt) returns two: the learner with their score out of the total, and a grade line. For a learner who scored 2 out of 5, that is "Mina: 2 of 5" and "Grade: F".
Those are the promises. Pin them down.
Task
Write test_report.py, covering what report.py produces.
Import headline and report_lines from report, and whatever you need from quizapp.models. Write three tests:
test_the_headline_names_the_quiz_and_its_points: for a quiz titled “Chapter 13 Review” worth 5 points,headlinereturns"Chapter 13 Review (5 points)".test_the_report_names_the_learner_and_the_score: build a real quiz and attempt where Mina scores 2 out of 5, and assert the first line is"Mina: 2 of 5".test_the_grade_line_reports_the_letter: assert the second line of that same report is"Grade: F".
Use real Quiz, Question, and Attempt