Skip to main content

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 and processNextGradeRow. 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 return true and then you can call processNextGradeRow to get that next value.
  • If there are no more rows available, hasMoreEntries will return false and processNextGradeRow 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. When processNextGradeRow is called, it returns the first available grade.
  • A followup hasMoreEntries returns true and the processNextGradeRow 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 of equals that reflects the fact that they are equal when their respective fields are equal. And along with it they need an implementation of hashCode.
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 a Scanner 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 a from... 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.