Assertion Extensions for JUnit

by: Tony Morris

AlphaWorks Package Allows you to Perform Unit Testing with Complex Assertions More Easily
JUnit lets you test software code units by making assertions that the intended requirements are met, but these assertions are limited to primitive operations. IBM Software Engineer Tony Morris fills the gap by introducing Assertion Extensions for JUnit, which provides a set of complex assertions that execute within the JUnit framework. Follow along as the author shows you how using this new package from alphaWorks can increase the reliability and robustness of your Java™ software.

The popular JUnit automated unit-testing framework gives you a way to test software code units by making assertions that your code meets the intended requirements. However, these assertions are limited to primitive operations, such as "assert equal on two variables" and "assert not null on a reference variable." The primitive JUnit assertions are useful, but they don't represent the many complex assertion abilities you need in real-world software unit-testing scenarios.

Assertion Extensions for JUnit (JUnitX), an extensions package for the JUnit framework, is available for download from alphaWorks (see Resources). It provides the required implementation of many common complex assertions. Rather than write a complex JUnit test case for an assertion, you can call a JUnitX method to make that assertion from the same context -- with no additional setup. JUnitX also asserts that it functions as documented by including its own JUnit self-test suite. This reassures you that JUnitX is making its assertion according to the JUnitX documentation; if a unit test fails, you know it's the software code unit that has failed, not the JUnitX test implementation asserting a false failure.

A typical scenario in which JUnitX would be beneficial involves the contracts set out by the java.lang.Object class's equals(Object) and hashCode() methods. You're often required to adhere to these contracts in the classes you develop. Using JUnit to make the assertion that you've adhered to them would require you to develop complex unit-test cases, which are prone to error. In contrast, making the assertions using JUnitX is as simple as creating a factory implementation that returns instances of your class and calling the JUnitX assertEqualsContract and assertHashCodeContract methods from your unit-test case.

Getting Started with JUnitX
Effective use of JUnitX requires a minimal learning curve. If you know how to use JUnit automated unit testing framework directly, you'll find it easy to use the JUnitX extensions package. The following steps will get you started:

1. If you haven't already done so, set up an environment for running JUnit test cases. You might consider following the process detailed in "Automating the build and test process" (see Resources).

2. Download the JUnitX package (see Resources) and extract the JUnitX archive to a directory of your choice.

3. Make the lib/junitx.jar file available to the class loader that will perform the JUnit tests.

Now you can make method calls on the junitx.framework.Assert class to assert functionality in a way similar to how you'd use the junit.framework.Assert class in a typical JUnit test environment. The JUnitX online API documentation (see Resources) provides a detailed description of the available method calls on the junitx.framework.Assert class.

Another JUnitX
The JUnitX package described in this article is unrelated to another project of the same name from Extreme Java, which allows testing of private and protected classes, methods, and variables.

Use Case Scenario
Suppose you've been given the requirement to implement a class that represents a person. The Person class requires three attributes: the person's title, first name, and surname. The title attribute can be one of a finite set of possible values -- MR, MS, and MRS -- so you intend to implement a Title class using the Typesafe Enumeration design pattern. The UML diagram in Figure 1 shows these requirements as they currently stand.

Figure 1. UML diagram of requirements


Listing 1 shows the source code for these requirements:

public class Person {

private Title title;
private String firstName;
private String surName;
}


public class Title {
public static final Title MR = new Title();
public static final Title MS = new Title();
public static final Title MRS = new Title();

// private constructor to prevent outside instantiation
private Title() {

}
}
The source code now needs to become robust and fully functional by meeting a more specific set of requirements, such as the following typical ones:

• The Person class overrides the equals(Object) and hashCode() methods according to their contracts so that it can be used effectively in Collection types.

• The Person class implements the java.io.Serializable interface and serializes and deserializes without producing any errors.

• The Person class is not declared final, so that it can be subclassed.

• The Title class is declared final because it doesn't expose any constructors (which implies that it cannot be subclassed). It's good practice to document the acknowledgment of this design decision so that the final modifier can be seen in the source code and generated API documentation.

• The Title class implements the java.io.Serializable interface, serializes and deserializes without producing any errors, and serializes to the same instance (in accordance with the requirements of the Typesafe Enumeration design pattern).

• The Title class has a private default (no argument) constructor to prevent instantiation from outside the class.

You can assert all of these requirements easily by using JUnitX in a JUnit test environment. The source code in Listing 2 is a set of JUnit test cases to assert that all of the requirements are met using JUnitX capabilities:

Listing 2. JUnit test cases using JUnitX assertions

import junit.framework.TestCase;

import junitx.framework.ObjectFactory;
import junitx.framework.Assert;
import java.io.Serializable;
import java.lang.reflect.Constructor;

public class TestRequirements extends TestCase {
public void testPersonEqualsAndHashCodeContract() {
// Different surnames should be unequal.
ObjectFactory factory = new ObjectFactory() {
public Object createInstanceX() {
return new Person(Title.MR, "Bob", "Brown");
}

public Object createInstanceY() {
return new Person(Title.MR, "Bob", "Smith");
}
}

// Make sure the object factory meets its contract for testing.
// This contract is specified in the API documentation.
Assert.assertObjectFactoryContract(factory);
// Assert equals(Object) contract.
Assert.assertEqualsContract(factory);
// Assert hashCode() contract.
Assert.assertHashCodeContract(factory);
}

public void testPersonSerialization() {
// Assert that the Person class directly implements Serializable.
Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
// Assert that the Person instance can be serialized and deserialized without errors.
Assert.assertSerializes(new Person(Title.MR, "Joe", "Blog"));
}

public void testPersonNotFinal() {
// Assert that the Person class is not declared final.
Assert.assertNotFinal(Person.class);
}

public void testTitleFinal() {
// Assert that the Title class is declared final.
Assert.assertFinal(Title.class);
}

public void testTitleSerialization() {
// Assert that the Title class directly implements Serializable.
Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
// Assert that the Title instances can be serialized and deserialized without errors.
Assert.assertSerializes(Title.MR);
Assert.assertSerializes(Title.MS);
Assert.assertSerializes(Title.MRS);
// Assert that serialization results in the same instance.
Assert.assertSerializesSame(Title.MR);
Assert.assertSerializesSame(Title.MS);
Assert.assertSerializesSame(Title.MRS);
}

public void testTitleConstructor() {
// Assert that the Title class has a default constructor.
Assert.assertClassHasConstructor(Title.class, null);
try {
// Get the default constructor.
Constructor con = Title.class.getDeclaredConstructor(null);
// Assert that the default constructor is declared private.
Assert.assertPrivate(con);
}
catch(NoSuchMethodException nsme) {
// Should never get here, even when test fails.
throw new IllegalStateException();
}
}
}
The Person and Title classes in Listing 1 won't pass the test cases in Listing 2, because they don't meet all of the additional requirements. You can now develop the classes to meet the new requirements so that the unit test cases pass, which implies that the requirements have been met. Listing 3 shows an example of an implementation that meets the specified requirements:

Listing 3. Revised classes meeting additional requirements

import java.io.Serializable;


public class Person implements Serializable {
private Title title;
private String firstName;
private String surname;

public Person(Title title, String firstName, String surname) {
this.title = title;
this.firstName = firstName;
this.surname = surname;
}

public boolean equals(Object o) {
// Performance optimization only.
if(this == o) {
return true;
}

if(o == null) {
return false;
}

if(!(o instanceof Person)) {
return false;
}

Person p = (Person)o;
return title == p.title && firstName.equals(p.firstName) && surname.equals(p.surname);
}

public int hashCode() {
final int oddPrime = 461;
int result = 73;
result = result * oddPrime + title.hashCode();
result = result * oddPrime + firstName.hashCode();
result = result * oddPrime + surname.hashCode();
return result;
}
}


import java.io.Serializable;

public final class Title implements Serializable {
public static final Title MR = new Title();
public static final Title MS = new Title();
public static final Title MRS = new Title();
private static int nextIndex = 0;
private final int index = nextIndex++;
private static final Title[] VALUES = new Title[]{MR, MS, MRS};

private Title() {

}

// Ensure that the same instance is returned when deserialized.
Object readResolve() {
return VALUES[index];
}
}
If you run the JUnit test cases on the classes in Listing 3, the tests pass, so you can conclude that your code meets the given requirements.

Conclusion
If you were to try to make the assertions in the use case scenario on your code without JUnitX, you would have a considerably larger amount of work to do. A failure of your test case could easily imply an incorrectly constructed test case, rather than an incorrect software code unit, which would take even more work to diagnose. If a JUnitX test case fails and the fault in your software code unit isn't immediately obvious, you can read the source code of the included self-test suite to learn what a code unit that passes the test case looks like.

The JUnitX package will continue to evolve as demand for more functionality is required. If you have any suggestions to enhance the package's functionality, or to report a bug, please feel free to contact the author.

Resources
• Download Assertion Extensions for JUnit from alphaWorks.

• Learn all about the open source JUnit automated unit testing framework.

• " Automating the build and test process" (developerWorks, 2001) shows how to use Ant and JUnit to automate and customize the build and test process

• " Control your test-environment with DbUnit and Anthill" (developerWorks, 2004) explains how to use DbUnit in conjunction with JUnit to control the test environment end-to-end by setting up database state before each test.

• Check out " Use Jython to build JUnit test suites" (developerWorks, 2004) to see how to use Python to feign statically defined JUnit TestSuite classes.

• " Incremental development with Ant and JUnit" (developerWorks, 2000) helps you use unit testing to improve your code in small steps.

• Find hundreds more Java technology resources on the developerWorks Java technology zone.

• Visit the Developer Bookstore for a comprehensive listing of technical books, including hundreds of Java-related titles.

Article published Thursday, 5th May 2005
© 2008 NetVisits, Inc. All rights reserved.