Saturday, February 14, 2009

Ipsedixit - Java unit testing with less code

As software engineers, we don't want to spend a lot of time performing repetitive tasks. Repetitive tasks in coding can almost always be solved through some software solution. Smart IDE's, tools for refactoring, and libraries to assist database access are some examples of how software helps reduce duplication.

Unit testing has gotten a lot of focus in the last few years, primarily from the Agile/XP practice of Test-Driven Development. Arguably, TDD is now mainstream practice, not some crazy XP idea that only those mad Agile evangelists espouse on their blogs. In terms of tool support, we have JUnit, TestNG, EasyMock, JMock, and a lot of other tools to help us write and run tests.

While thats all great, there is still the issue of test data. I've seen a lot of unit tests contain a lot of setup code just to create suitable test data.

Consider a reasonably simple JUnit4/EasyMock test for a service, where we want our service to call out to a DAO which returns a particular object:
public class MyServiceUnitTest {
private MyDao myDao;
private MyService myService;;

@Before
private void setup() {
myDao = EasyMock.createMock(MyDao.class);
myService = new MyService(myDao);
}

@Test
public void canGetDataFromDaoAndReturn() {
Serializable primaryKey = "Don't care";
MyDomainObject myDomainObject = new MyDomainObject();

EasyMock.expect(myDao.load(primaryKey)).andReturn(myDomainObject);
EasyMock.replay(myDao);

MyDomainObject result = myService.findById(primaryKey);
Assert.assertSame(myDomainObject, resut);
EasyMock.verify(myService);
}
}
Now there's nothing actually wrong with this test, but taking TDD to the letter (fake it till you make it), the implementation of the class would actually be wrong. It would look like:
public class MyService {
...

public MyObject findById(Serializable id) {
return myDao.load("Don't Care");
}
}
The only way to force the correct implementation is to triangulate. That is, to perform the same test but pass in a different ID. That sounds like duplicated effort to me!

Whilst the example above is trivial and somewhat contrived, there is one other key point I'd like to make: I had to set up some test data to get the tests to work. There are two pieces of data that are used.

The first is the value of "primary key" that is an instance of Serializable. Actually, that's only partly true, it's a String as far as the test is concerned (the value is "Don't Care"). I could have chosen an Integer, or Float, or any type that implements Serializable. That's a fatal flaw in my test because I risk getting a ClassCastException if somewhere, someone assumes that ID's are String's and tries to cast it as such.

The second piece of test data is the instance of MyDomainObject. Lucky in our case it was easy to create, but what if MyDomainObject had several arguments on the constructor? Just for fun, let's put on the constructor a javax.xml.transform.Transformer, org.hibernate.Session and javax.mail.Session. And since the domain object is in a JAR that you don't have the source to, you can't just go and change the API :-)
   ....

@Test
public void canGetDataFromDaoAndReturn() {
Serializable primaryKey = "Don't care";
Transformer transformer = TransformerFactory.newTransformer();
org.hibernate.Session hibernateSession = SessionFactory.openSession(); // uh oh, this needs a hibernate config or it will throw an exception!
javax.mail.Session mailSession = ...// umm, how do I get one of these things without JavaEE?

MyDomainObject myDomainObject = new MyDomainObject(transformer, hibernateSession, umm...);

// continue testing ...
}
Not quite so easy to create one for the purpose of testing, is it? You could spend quite a bit of time messing around with this, just to set up your test data. Not only is that tedious, but when the next person looks at the test they'll need to interpret all that setup code, which makes the test harder to understand.

Enter Ipsedixit. Ipsedixit is a tool that takes care of thinking up that test data for you. It even knows how to "instantiate" interfaces and classes, and provides them to your test, so in the example above it doesn't matter what MyDomainObject needs on the constructor.

Using Ipsedixit, lets have a look at the unit test again...
@RunWith(JUnit4IpsedixitTestRunner.class)
public class MyServiceUnitTest {
private MyService myService;
@Mock private MyDao myDao;
@Arbitrary private MyDomainObject myDomainObject;
@Arbitrary private Serializable primaryKey;

@Before
private void setup() {
myService = new MyService(myDao);
}

@Test
public void canGetDataFromDaoAndReturn() {
EasyMock.expect(myDao.load(primaryKey)).andReturn(myDomainObject);
EasyMock.replay(myDao);

MyDomainObject result = myService.findById(primaryKey);
Assert.assertSame(myDomainObject, resut);
EasyMock.verify(myService);
}
}
This has the advantage of the previous test in a number of ways:
  • I don't have to construct any test data at all. The @Arbitrary annotation will make Ipsedixit provide an instance for you.
  • I don't need to set up my mock. Ipsedixit can create one automatically using the @Mock annotation. You may also use Atunit for automocking, if you prefer.
  • My test code is reduced, meaning the intent of the test is clearer
For common Java types and primitives, Ipsedixit can provide random values. For example placing an @Arbitrary annotation before an int field will make Ipsedixit populate that field with a random number. String fields get random Strings. You can even customise how the random value is provided (ie, a string of a particular length, or a number in a certain range).

For other types, such as the ones in our example, Ipsedixit will provide a dynamic proxy. So, for the Serializable we get a JDK Proxy, and for MyDomainObject we get a CGLIB proxy. It doesn't actually matter what they are, but the point is that they are objects of the types we require.

Ipsedixit does not require any particular testing framework to run, but there are integration points for JUnit 3, JUnit 4, and Spring-Test. It would not be a hard ask to integrate TestNG and other frameworks either.

You might be surprised at how seemingly complicated tests can be simplified by introducing Ipsedixit. Give it a go today!

1 comment: