Section 8.5 Clean up your tests
The most important thing to keep in mind when it comes to tests is this:
Treat your tests with as much respect as your code. Refactor and clean them up, keep them as readable, clear and simple as they can be.
Let’s apply this principle to our test. First off, we tend to inline local variables used only once if the extra name they bring to the table isn’t helpful. In our case the
points
variable could be inlined. We kept it separate so that we can discuss the arrange-act-assert parts of the test, but since that discussion is done let’s make the test more readable:@Test
public void letterGradesHaveAppropriatePoints() {
Grade grade = new Grade("A");
assertEquals(4.00, grade.getPoints(), DELTA);
}
Come to think of it, I probably would also inline the
grade
variable:@Test
public void letterGradesHaveAppropriatePoints() {
assertEquals(4.00, new Grade("A").getPoints(), DELTA);
}
Now you may argue we just made the test harder to read, and that’s partly true. But what we are missing is actually not explanatory variables so much as an explanatory method, that tells us what the above assertion does: It is supposed to assert that a grade given by a certain letter has certain points. If that is what we want to say, we should make it plain as day, by extracting that line in a method that takes the letter and the expected points as parameters, like so:
@Test
public void letterGradesHaveAppropriatePoints() {
assertPointsForLetter("A", 4.00);
}
private void assertPointsForLetter(String letter, double points) {
assertEquals(points, new Grade(letter).getPoints(), DELTA);
}
So we hide the complexity in the helper method. And there, if desired, you could use a
grade
explanatory variable. But it seems fine to me as it is.Say what you mean, mean what you say.
And now that we have expressed so succinctly in one line of test code what we expect, we can add to that test one line for each letter-points pair. This is technically violating the single-assert rule, but it is exactly the case described as an exception above: We create a grade and read a value from it, and there are no side-effects and we are not reusing the same grade object. Having multiple of those statements within the same test reads nicely, so we will do it, like so:
@Test
public void letterGradesHaveAppropriatePoints() {
assertPointsForLetter("A", 4.00);
assertPointsForLetter("A-", 3.67);
assertPointsForLetter("B+", 3.33);
assertPointsForLetter("B", 3.00);
assertPointsForLetter("B-", 2.67);
assertPointsForLetter("C+", 2.33);
assertPointsForLetter("C", 2.00);
assertPointsForLetter("C-", 1.67);
assertPointsForLetter("D+", 1.33);
assertPointsForLetter("D", 1.00);
assertPointsForLetter("D-", 0.67);
assertPointsForLetter("F", 0.00);
assertPointsForLetter("W", 0.00);
}
So I have succinctly and in one place described all the different grade cases. Even someone without much programming experience could read these lines and make some sense of what they are trying to say. And that’s our goal, make the tests readable.