This post will describe how to achieve 100% code coverage within a typical JPA domain model with an incremental cost of a few lines of code per class and absolutely no need to update the tests when changing the model classes.
If there’s one thing that we can be better at as software developers, it’s writing automated tests. It’s the most under-appreciated step in a software development process and it’s usually the first step to get cut when people think that they are in a crisis.
Ironically, diligent testing can help avoid those crisis situations.
One of the biggest impediments to diligent testing is the human tedium involved with keeping up with testing and code coverage. There are parts of a codebase that test very slow, and there are parts of a codebase that invite testing.
As tests go, the thing developers typically want to focus on first is the action: service logic, new technologies, uncertainty, etc. That’s where all the excitement happens. The Test Driven Development folks thrive on defining a method’s behavior first, writing a test to look for that behavior, let the test fail and then implementing it and see the test pass. Even the TDD stalwarts may look at writing automated tests for a domain model and skip it.
Domain code is far away from the action. It’s usually boring code to test. The methods could not be more predictable though: getters and setters. The getters get the value of a field. The setters set the value of a field.
There are a few other methods on a domain object also: equals, hashCode and toString. Those methods are important and often overlooked. The equals method does what one would expect it to do, it compares the state of the object to another object. The hashCode method is kind of a short circuit for equals. It’s an inexpensive way to compare the state of two objects. Many persistence frameworks will compare the hashCode values of two objects before invoking an equals method.
An improperly implemented hashCode or equals implementation can be an insidious cause for data corruption. The data corruption often operates quietly by failing to delete values from a list or failing to add values to a list.
Ensuring that hashCode and equals operates correctly is a good way to avoid this expensive and tricky defect from causing data corruption.
Even with great traditional testing when a Bean is defined, the biggest risk to having an improperly implemented equals and hashCode comes when an attribute is added to that Bean. It’s very easy for someone to overlook adding that field to the equals and hashCode logic at the time when the field is added.
The flaw with traditional equals and hashCode testing is it can only test with the fields the Bean has at the time the test is written. Some changes can be made independently to the Bean without causing the test to fail.
I skate to where the puck is going, not to where it has been. —Wayne Gretzky
One could follow the puck and write a test for the Bean where it is. The Metatesting approach is to write tests that will work no matter what is added to, or removed from, the Bean.
Beans have rules. Each field needs a getter and a setter. The equals and hashCode methods should both follow rules based on the state of an object. The toString method should produce a String representation of the Bean.
All of these rules follow the fields in the Bean. For each field there must be a getter and a setter. Each of those fields, that are not annotated @Transient, should affect the equals and hashCode methods.
The Bean rules form a pattern. This pattern is used to write a single abstract test that can be used by Bean specific tests to ensure that for each field on the object: the getters get, the setters set, the equals tests for equality, hashCode does its thing, and toString represents the state of the object.
Tests written with Groovy, and by extension Spock, have the ability to easily view the fields of an object. There is a method in DefaultGroovyMethods, getProperties that will return all of the fields. Aside from two fields, class and metaClass, the Bean should have a getter and setter for each of the fields.
By iterating over an object’s properties, a collection can be built for the getters, setters, and the argument types for each.
With a collection of each getter, setter, and field type, one has all they need to test hashCode and equals. The pattern takes two instances of the Bean with the same state. For each field, the test will first assert that the two instances produce the same hashCode and the equals method on one to the other returns true, and vice versa; next the setter for that field will be invoked with a value of the type; the same test for equality is invoked, but it expects a false value for the assertions; next the setter is invoked with the same value on the other object and the tests are performed with the expectation that they are equal.
By performing this process almost all of the code in the Bean is functionally exercised. It demonstrates that the getters get, the setters set, and equals and hashCode do the right thing.
The same approach can be followed to test the toString method.
With all the heavy lifting performed in an Abstract Spock Spec, the testing of the actual Bean becomes very simple.
In the Abstract Spec, there are two public methods: testEqualsAndHashCode and testToString. Invoking them in the Bean’s test class is as simple as a 1 line invocation in an expect: block.
The test code will fail should a field be added to the model without adding the appropriate logic to equals and hashCode. The cost of adding a test is minimal once the Abstract Spec is implemented.
Additional logic can be added to the Abstract Spec as well. In the case where a field annotated with @Transient is intended to hold data, but not contribute to the state of the object, some logic in the Abstract spec can look at each field and identify those with the @Transient annotation.
In those instances, the field is skipped in the equals and hashCode testing.
The Convention Over Configuration trend in software development creates predictable patterns in source code. Thinking about expectations for coding through compliance with the patterns invites the opportunity to verify the compliance with testing. This is an example of using the expectation from patterns to create tests that provide complete code coverage with low maintenance overhead and quick runtime feedback.
The source code for this post is available on Gitlab.
Header image courtesy of Unsplash.