Section 8.6 A more complex test example
Subsection 8.6.1 Tests via wishful thinking
Now let’s write some tests for the
GradeReader
class. This requires a bit more work:- There is some input text that needs to be processed via a scanner, so we need to create that scanner.
- The
GradeReader
interaction protocol involves two methods working in tandem:hasMoreEntries
andprocessNextGradeRow
. We really cannot and should not test these in isolation; it is their interaction that matters.
This is an important thing to keep in mind:
When a class’s protocol involves interaction between multiple methods, test these methods in tandem.
We used a new word there, protocol. What I mean by this is simply the expected behavior of the methods in the class, namely:
- If there are more rows available,
hasMoreEntries
will returntrue
and then you can callprocessNextGradeRow
to get that next value. - If there are no more rows available,
hasMoreEntries
will returnfalse
andprocessNextGradeRow
has unpredictable behavior when called.
For example, to properly test that our class operates correctly in a situation where there are two grade entries, it should do the following:
- Initially
hasMoreEntries
returns true. WhenprocessNextGradeRow
is called, it returns the first available grade. - A followup
hasMoreEntries
returns true and theprocessNextGradeRow
after that returns the next grade. - The next
hasMoreEntries
returns false.
It is this sequence of events that our test is supposed to ascertain. As such, our test will have to violate the normal single-assert rule, and have a sequence of act-assert interactions. But we will extract it away in a helper method to make it more clear. Here’s how I would like our test to read. I call this test-writing technique wishful thinking. We start by writing what we would like our test to look like, then we fill in the details.
@Test
public void canReadTwoRowInputProperly() {
GradeReader reader = readerFor("CS 121 A-\n MAT 112 B");
assertHasNextGrade(reader, "A-");
assertHasNextGrade(reader, "B");
assertNoNextGrade(reader);
}
So I have a method to create a reader from an input string. That involves creating a
Scanner
first, but I’ll hide that behind my helper method, it would not improve the readability of the test to have that out in the open. Then I have two helper methods that express my two key ideas:- There is a next element and it is the grade ...
- There is no next element
How would we implement these helper methods? Relatively easy:
private GradeReader readerFor(String input) {
return new GradeReader(new Scanner(input));
}
private void assertHasNextGrade(GradeReader reader, String letter) {
assertTrue(reader.hasMoreEntries());
assertEquals(new Grade(letter), reader.processNextGradeRow());
}
private void assertNoNextGrade(GradeReader reader) {
assertFalse(reader.hasMoreEntries());
}
Subsection 8.6.2 Aside: Equals method for value objects
This test of ours will currently fail, for a simple reason: Currently two
Grade
objects are not equal simply because they have the same letter grade stored in them. Only if they are literally the same object in memory, created once then passed around.Value objects need an implementation ofequals
that reflects the fact that they are equal when their respective fields are equal. And along with it they need an implementation ofhashCode
.
We have not talked about these two methods much, but essentially
hashCode
is used when those objects are to be used as keys, in a HashMap
for example. A key requirement to satisfy is that if two objects are .equals()
then they should have the same hashCode()
. Java provides us an easy way to create this method, by relying on Objects.hash
. Here is a standard implementation of these two methods (many IDEs will auto-insert one for you when asked):public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Grade grade = (Grade) o;
return Objects.equals(letterGrade, grade.letterGrade);
}
public int hashCode() {
return Objects.hash(letterGrade);
}
In general the
hashCode
implementation simply calls Objects.hash
with a list of arguments corresponding to the class fields. The equals
implementation has a bit more work to do:- First of all, it needs to accept an arbitrary other object
o
for comparison. - It starts by checking if those two objects are literally the same memory references. That’s what
==
does. If they are, then they are of course equal. - Otherwise, it checks to see if the other object is of the same class as our object. If it is not, then there’s no point trying to compare them further.
- If it is the same class, then we down-cast it to an object of our class, with the
(Grade) o
line. - Lastly, we compare the fields of our two objects, and if they all match then the objects are equal. In our case there is only one field to worry about, otherwise we would have had a whole bunch of
&&
operations in that return line.
With this change in mind, our tests will now work. And we also have a clean template to write a number of similar tests.
Subsection 8.6.3 Aside: Creating objects, Static Factory methods
Our
GradeReader
testing brought up an interesting situation: We might want to create such a reader from different starting points:- Maybe we have a
Scanner
instance already. Then we can call the constructor directly. - Maybe we have an input
String
in mind. We would then need to create aScanner
instead based off that input string, then pass that to the constructor. - We might have an
InputStream
, like the situation if we were to read from a file. In that case we can build a `Scanner out of that stream, and use the scanner.
It might be nice to spare the user of our
GradeReader
class these extra steps. We should provide them with convenience methods for creating a GradeReader
out of any of those initial settings. A standard way to do that is to have multiple constructors:Multiple constructors may be defined when the needed initial arguments for our class can vary, or when we would like to provided default initial values for them.All constructors should do their job by eventually calling a single constructor, the one that expects the most complete set of parameters.
You can call another constructor by using
this
as a method, like so:public GradeReader(InputStream stream) {
this(new Scanner(stream));
}
public GradeReader(String input) {
this(new Scanner(input));
}
This can work just fine. But there is a better way, static factory methods:
Static factory methods are static methods that return instances of the class, by calling the appropriate constructor. They are often named in afrom...
format to indicate the type of input they utilize. In general they don’t follow the normal method-naming convention of starting with a verb, instead they use an adjective-based form that describes the type of object created.
As an example, a list class might have two static factory methods: a
List.empty()
method that creates an empty list, or a List.fromValues(...)
method that takes as input any number of values.In our case we will create three static factory methods:
fromScanner
, fromStream
, and fromString
.There is a refactoring that lets us do that, called Replace Constructor with Factory Method.
Mechanics of Replace Constructor with Factory Method refactoring:
Identify the constructor that you wish to replace. Create a new static method with same parameters and access modifier as the constructor and with the class as return type. In the new method, return a call to the constructor, passing to it all the parameters. Find all places where the old constructor was used, and replace them with a call to this factory method. Make the old constructor private. (This is also a great way to find out all the places where the constructor was called in order to replace them) If the constructor is itself a wrapper around another constructor, simply inline and remove it.
In our case, this would give rise to the following methods:
public static GradeReader fromScanner(Scanner scanner) {
return new GradeReader(scanner);
}
public static GradeReader fromString(String input) {
return new GradeReader(new Scanner(input));
}
public static GradeReader fromStream(InputStream stream) {
return new GradeReader(new Scanner(stream));
}
And all calls to the constructor will change accordingly (in
Main
and in GradeReaderTest
in our case). For example in our GradeReader
test we will now have:private GradeReader readerFor(String input) {
return GradeReader.fromString(input);
}
And at this point, we could probably inline this method anyway, now that the process of creating the
Scanner
has moved to the GradeReader
class. And this is an important lesson, worth its own section below, but it boils down to this:Making your tests clean and readable often helps you discover new methods and functionality that your class should be providing.
It was our desire to keep our tests clean that led us to consider that creating a
GradeReader
directly from an input string would be a good idea. We had to do it in our test anyway, and on hindsight it would fit well in the actual class as well.