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.

These are

  • equals
  • hashCode
  • toString (i.e. check no runtime exception is raised)
  • compareTo
  • Serialization

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 null.

public final class Point {
  private final Integer x;
  private final Integer y;
 
  public Point(final Integer x, final Integer y) {
    this.x = x;
    this.y = y;
  }
}

equals and hashCode

So let’s add a equals method to the class.

import org.apache.commons.lang.ObjectUtils;
 
public final class Point {
  private final Integer x;
  private final Integer y;
 
  public Point(final Integer x, final Integer y) {
    this.x = x;
    this.y = y;
  }
 
  @Override
  public int hashCode() {
    int result = 17;
    result = 37 * result + ObjectUtils.hashCode(x);
    result = 37 * result + ObjectUtils.hashCode(y);
    return result;
  }
 
  @Override
  public boolean equals(final Object object) {
    if (this == object) {
      return true;
    }
    else if (object == null || getClass() != object.getClass()) {
      return false;
    }
 
    final Point other = (Point) object;
 
    return (ObjectUtils.equals(this.x, other.x)
        && ObjectUtils.equals(this.y, other.y));
  }
}

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 equals:

  1. It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  2. 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.
  3. 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.
  4. 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.
  5. For any non-null reference value x, x.equals(null) should return false.

And for hashCode:

  1. 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.
  2. 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.
  3. 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:

import org.junit.experimental.theories.DataPoint;
import static java.lang.Integer.MIN_VALUE;
import static java.lang.Integer.MAX_VALUE;
import de.smartics.util.test.theories.ObjectTheories;
 
public class PointTest extends ObjectTheories {
  @DataPoint
  public static final Point MIN = new Point(MIN_VALUE, MIN_VALUE);
  @DataPoint
  public static final Point MIN_MAX = new Point(MIN_VALUE, MAX_VALUE);
  @DataPoint
  public static final Point MAX = new Point(MAX_VALUE, MAX_VALUE);
  @DataPoint
  public static final Point EQUAL = new Point(MAX_VALUE, MAX_VALUE);
}

The theory tests that equals and 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 toString implementation:

@Override  public String toString() {
  final StringBuilder buffer = new StringBuilder();
  buffer.append(x.intValue()).append(", ").append(y.intValue());
  return buffer.toString();
}

And add a new data point to our test:

@DataPointpublic static final Point NULL = new Point(null, null);

And see the toString test fail:

We just wanted to show that the theories really find bugs.(smile)

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):

@Override
public String toString() {
  final StringBuilder buffer = new StringBuilder();
  buffer.append(x).append(", ").append(y);
  return buffer.toString();
}

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.(sad)

With all data points as shown above:

Yes, it is! (smile)

compareTo

Implementing a Comparable has similar constraints as equals to be met.

We add the comparable interface to our Point class:

 public final class Point implements Comparable<Point> {
 ...
  public int compareTo(final Point o) {
    int compare = ObjectUtils.compare(this.x, o.x);
    if (compare == 0) {
      compare = ObjectUtils.compare(this.y, o.y);
    }
    return compare;
  }

Here is the solution using CompareToTheory. Let’s start with just one data point:

import static java.lang.Integer.MIN_VALUE;
import org.junit.experimental.theories.DataPoint;
import de.smartics.util.test.theories.CompareToTheory;
 
public class PointCompareToTest extends CompareToTheory<Point> {
  @DataPoint
  public static final Point MIN = new Point(MIN_VALUE, MIN_VALUE);
}

 

As we can see, the test compareToIsTransitive signals a test failure, since one data point is not enough to check transitivity for compareTo.

So let’s add two additional data points:

import static java.lang.Integer.MAX_VALUE;
import static java.lang.Integer.MIN_VALUE;
import org.junit.experimental.theories.DataPoint;
import de.smartics.util.test.theories.CompareToTheory;
 
public class PointCompareToTest extends CompareToTheory<Point> {
  @DataPoint
  public static final Point MIN = new Point(MIN_VALUE, MIN_VALUE);
  @DataPoint
  public static final Point MIN_MAX = new Point(MIN_VALUE, MAX_VALUE);
  @DataPoint
  public static final Point MAX = new Point(MAX_VALUE, MAX_VALUE);
}

Again, with only some edge values are defined, JUnit theories do their work.

You like typos? (smile)

public int compareTo(final Point o) {
  int compare = ObjectUtils.compare(this.x, o.x);
  if (compare == 0) {
    compare = ObjectUtils.compare(this.x, o.y);  // <-- TYPO!
  }
  return compare;
}

Gotcha!

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!

Serialization

You might already guess, how the test case based on SerializationTheory looks like:

 

import static java.lang.Integer.MAX_VALUE;
import org.junit.experimental.theories.DataPoint;
import de.smartics.util.test.theories.SerializationTheory;
 
public class PointSerializationTest extends SerializationTheory {
  @DataPoint
  public static final Point NULL = new Point(null, null);
  @DataPoint
  public static final Point MAX = new Point(MAX_VALUE, MAX_VALUE);
}

This time we do test-first and run into an error: our class does not implement the Serializable interface.

Let’s add the missing interface and we have finally the complete Point class:

import java.io.Serializable;
import org.apache.commons.lang.ObjectUtils;
 
public final class Point implements Serializable, Comparable<Point> {
  private static final long serialVersionUID = 1L;
 
  private final Integer x;
  private final Integer y;
 
  public Point(final Integer x, final Integer y) {
    this.x = x;
    this.y = y;
  }
 
  public int compareTo(final Point o) {
    int compare = ObjectUtils.compare(this.x, o.x);
    if (compare == 0) {
      compare = ObjectUtils.compare(this.y, o.y);
    }
    return compare;
  }
 
  @Override
  public int hashCode() {
    int result = 17;
    result = 37 * result + ObjectUtils.hashCode(x);
    result = 37 * result + ObjectUtils.hashCode(y);
    return result;
  }
 
  @Override
  public boolean equals(final Object object) {
    if (this == object) {
      return true;
    }
    else if (object == null || getClass() != object.getClass()) {
      return false;
    }
 
    final Point other = (Point) object;
 
    return (ObjectUtils.equals(this.x, other.x)
        && ObjectUtils.equals(this.y, other.y));
  }
 
  @Override
  public String toString() {
    final StringBuilder buffer = new StringBuilder();
    buffer.append(x).append(", ").append(y);
    return buffer.toString();
  }
}

Let’s run the test one more time to be sure:

You may ask yourself, how do I get this?

(wink)

If you are using Maven, integration in your build process is quite simple:

<dependency>
  <groupId>de.smartics.util</groupId>
  <artifactId>smartics-test-utils</artifactId>
  <version>0.3.3</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.2</version>
  <scope>test</scope>
</dependency>

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.


Link

Link

Posts

Tagcloud

Loading tagcloud ...