Unit-Testing is often a simple task, if you have small units that are designed to be tested in isolation easily. These units in common applications often have no complex business logic and the edge cases are quickly pinned down and dealt with individually, unit by unit.
But there are a couple of responsibilities (mostly methods) in Java that are to be tested the same way for each implementation.
toString(i.e. check no runtime exception is raised)
We will show in this article, how these responsibilities can be tested declaratively without writing individual test cases using JUnit theories.
For demonstration we will add methods to the following simple point class. For brevity we do not add getters to the fields and omit any Javadoc comments. For some obscure reasons we allow the coordinates to have a value of
equals and hashCode
So let’s add a
equals method to the class.
Since we allowed coordinate values to be
null, we use
ObjectUtils from Apache’s commons-lang library.
Usually, for every class that supportes
equals and therefore
hashCode, we have to write some tests with typical edge values. What has to be tested is stated in the API documentation for
Object. And this is the same for all implementations.
These are the constraints for
- It is reflexive: for any non-null reference value x, x.equals(x) should return true.
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
- It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
- For any non-null reference value x, x.equals(null) should return false.
- Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
- It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
This adds up to a couple of unit tests for each class. Writing these tests is cumbersome and error-prone.
Fortunately JUnit provides a features called Theories that help to specify the rules given above once and run these rules (or theories) against any implementation. All you have to do is to create instances for the edge values and tell JUnit to run the theories against them. So the work we have to do, using an implementation of ObjectTheories is this:
The theory tests that
hashCode are implemented as required by the rules of the
Object class shown above. This is the result of the run:
Everything’s green. You see the names of the tests that have been run. It also tests that the
toString method. We still only have the default implementation and now add the following
And add a new data point to our test:
And see the toString test fail:
We just wanted to show that the theories really find bugs.
An easy fix (a better fix may be to not allow
null values, but we do not want to make this article longer than it actually is – add a check to the constructor and the read method for serialization – we cover serialization in the next section):
At this point you may find code coverage a handy tool to check that every case has been checked by the theories. Let’s check if using a single data point is not enough:
No, it isn’t.
With all data points as shown above:
Yes, it is!
Comparable has similar constraints as
equals to be met.
We add the comparable interface to our
Here is the solution using
CompareToTheory. Let’s start with just one data point:
As we can see, the test
compareToIsTransitive signals a test failure, since one data point is not enough to check transitivity for
So let’s add two additional data points:
Again, with only some edge values are defined, JUnit theories do their work.
You like typos?
compareTo is no longer symmetric nor transitive.
One Word of Warning
Please note that it depends on your data points (and typos ;-)), which tests fail. For more complex data types, please check with a coverage tool that all paths have been covered!
Theories just check that the implementation adhere to the rules stated by the Java API.
In the example above we just check symmetry and transitivity. We do not check business rules as: is point (0, 1) really smaller than point (1,2)? The same is true for equals: we do not check if two instance that should be equal as to the business rules, are really equal.
Add these tests, if you need to!
You might already guess, how the test case based on
SerializationTheory looks like:
This time we do test-first and run into an error: our class does not implement the
Let’s add the missing interface and we have finally the complete
Let’s run the test one more time to be sure:
You may ask yourself, how do I get this?
If you are using Maven, integration in your build process is quite simple:
Note to update versions as appropriate.
The artifact is not available on the standard repositories. Please have a look at our instructions on how to access artifacts on our repository.
If you are not using Maven, please download the artifact from our repository and check the dependencies manually. Either by inspecting the pom.xml or the dependency report on the project’s site.
For more details on this library, please refer to smartics Test Utils.