Testing and Refactoring Object-oriented Programs · capstone
Capstone project: Project: Test and Refactor the Quiz Application
Eight lessons of tests, and now the reason for them.
Refactoring means changing how code is arranged without changing what it does. The definition is easy. What makes it possible in practice is being able to tell whether you have kept the second half of the bargain, and that is what your test files are for.
You have twenty-odd tests covering Question, Quiz, Attempt, the scoring
The problem, restated
Last lesson found it. The grade line is an idea about two numbers:
a score out of a total corresponds to a letter
but the only way to reach it is through an Attempt that knows a Quiz full of Questions. Testing “4 out of 5 is a B” took seven lines of arrangement, and six of them were about getting the numbers 4 and 5 into position.
Look at what report_lines does with its
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)}",
]
It pulls three values out of the attempt and then never mentions it again. Everything after that first unpacking is
The move: separate deciding from fetching
Give each line a function that takes what it needs, and let report_lines be the part that knows where those values live:
def grade_line(score, total):
return f"Grade: {grade_letter(percentage(score, total))}"
Now grade_line(4, 5) answers last lesson’s question directly, in one line, with no quiz in sight. report_lines keeps its job, which is to know that a score comes from attempt.score and a total from attempt.quiz.total_points, and to put the lines in order.
This is Chapter 6’s idea about responsibilities, arriving from a different direction. There it was a design argument. Here the tests found it for you.
The rules of the move
Do not change behavior. Not the wording, not the order, not the spacing. report_lines must return exactly what it returned before.
Keep the tests running the whole time. Not once at the end. After each step, so a failure names the step that caused it.
Do not touch the tests. The safety net only works if it is fixed while you move. Changing a test to match new output turns a caught mistake into a decision you made without noticing.
That last rule has one
Halfway through the refactoring, test_an_empty_quiz_still_reports fails. What is the right response?
What you are building
report.py ends up with four functions instead of two.
headline(quiz) stays exactly as it is.
score_line(learner, score, total) returns the first line: the learner, then the score out of the total.
grade_line(score, total) returns the second: the word Grade: and the letter, working out the fraction on the way.
report_lines(attempt) reads the three values off the attempt and returns the two lines, in order, by calling the other two.
When you are done, report_lines should contain no
Task
Refactor report.py so the two report lines can be built without an Attempt.
Keep headline(quiz) exactly as it is, and add two
score_line(learner, score, total)returns"Mina: 2 of 5"for("Mina", 2, 5).grade_line(score, total)returns"Grade: F"for(2, 5), working out the fraction itself.
Then rewrite report_lines(attempt) to read learner, score, and total_points off the attempt and return the two lines by calling those functions. When you are finished it should contain no
Nothing the program prints may change. Run tests as you go: all of your test files from this chapter have to stay green, and none of them may be edited.