Unit testing interface implementations
It's bugged me for a while about how best to go about unit testing classes that implement multiple interfaces where those interfaces are implemented by multiple classes or where a class extends another class and implements one or more interfaces.
What I want to avoid is in each test class implementing the the same set of tests for the behavior specified by the interface. That's not very DRY.
If your classes only implement a single interface then you can have a base class with the tests for the interface and then separate test classes derived from that for each class implementing the interface. The problem arises when your classes implement multiple interfaces - Java doesn't support multiple inheritance for classes so you can't have a base test case for each interface.
One answer with JUnit 4 is to use the parameterized test runner - I haven't thought about JUnit 3, if you're not using JUnit 4 because you're still using a JVM that doesn't support annotations JSE 6 is out now, catch up!
You need to have a test case for your interface and configure this to run with the parameterized runner. You also have a static factory method that provides the various implementations of the interface to be used in each test.
As an example, say the container I'm writing (I'm not really - I know there is Apache Geronimo, JBoss, Hivemind etc - this is just an example) supports services that it can start and stop, so I have an interface Service and the various services that I have, e.g. caching, lookup, logging etc, implement this interface.
-
/**
-
* Represents a service that the container can run.
-
*
-
*/
-
public interface Service {
-
-
/**
-
* Starts the service, may only be called if the service is not started.
-
*
-
* @throws IllegalStateException
-
* if called when the service is already started.
-
*/
-
public void start();
-
-
/**
-
* Indicates whether the service is started
-
*
-
* @return true if the service is started, false otherwise
-
*/
-
public boolean isStarted();
-
}
I have a test case for Service that tests that the various implementations of Service adhere to the general contract. I might have the following three tests:
-
public class ServiceTest {
-
-
private Service service;
-
-
@Test
-
public void shouldNotIndicatedStartedForNewObject() {
-
assertFalse(service.isStarted());
-
}
-
-
@Test
-
public void shouldStartIfNotAlreadyStarted() {
-
service.start();
-
assertTrue(service.isStarted());
-
}
-
-
public void shouldThrowExceptionIfStartedCalledOnAStartedService() {
-
service.start();
-
assertTrue(service.isStarted());
-
service.start();
-
}
-
}
If I have a caching and lookup service and I want to test that both adhere to the contract of the Service interface then I have to annotate the ServiceTest to indicate that it should run with the parameterized runner and provide an annotated factory method that returns a collection of arguments to run the tests with.
So the full testcase is:
-
@RunWith(Parameterized.class)
-
public class ServiceTest {
-
-
private Service service;
-
-
public ServiceTest(final Service service) {
-
this.service = service;
-
}
-
-
@Parameters
-
}
-
-
@Test
-
public void shouldNotIndicatedStartedForNewObject() {
-
assertThat(service, is(not(started())));
-
}
-
-
@Test
-
public void shouldStartIfNotAlreadyStarted() {
-
service.start();
-
assertThat(service, is(started()));
-
}
-
-
public void shouldThrowExceptionIfStartedCalledOnAStartedService() {
-
service.start();
-
assertThat(service, is(started()));
-
service.start();
-
}
-
}
The test classes for CachingService and LookupService now only need deal with testing the specifics of caching or lookups rather than testing adherence to the service specification.
I have yet to use this in production code on a large scale so we'll have to see how it works out, but it seems far better than having to replicate test code amongst several test cases.
May 22nd, 2007 at 12:22 am
Hello
We’ve got a non-profit site here with an article on Unit Testing in general, located at: http://coderslog.com/Unit_Testing_101
Hoping to get examples on there soon. The site itself is a journal of common code/configurations and other hard to find information. Should come in handy for building high performance large scale java applications and such.
December 12th, 2007 at 1:27 am
Don’t you think that enumerating all the interface implementations in the interface test is a bit wrong?
It seems to me it would be more correct to have some kind of a generic test suite that tests the interface contract and use it as a part of a test suite for each implementation.
December 12th, 2007 at 8:14 pm
I can’t comment on how well this works in practice as since I wrote the post I’ve been working solely with Ruby.
In the past I’ve alway tried to avoid suites as test cases inevitable accidentally get left out however in this case either way you’d have to remember to add an entry to something, either the suite or the interface testcase.
As I’ve not used suites much I’m not sure how go about telling the generic test which implementation of the interface to test, or I may have misunderstood what you meant.
One other small benefit that has just come to mind of having all the implementations of an interface listed in the test for the interface is that if you change the interface you just need to run the test for it to discover which classes are broken rather than having to remove all compiled class files and compiling everything to pick up which classes were dependent on the interface.
December 15th, 2007 at 7:26 am
Suites can be connected to form a tree. A suite for the interface contract can be added as a child to the suite that tests the class.
About telling which implementation of the interface to test, it is quite easy with JUint 3 where you are creating suites yourself. You can just add a method to the suite that will take an implementation factory and will propagate it into all contained tests. Then you need to do a call to supply the factory in addition to attaching the interface test suite as a child to your class test suite.
December 15th, 2007 at 7:30 am
For some reason I can not submit the rest of my post
December 15th, 2007 at 7:31 am
It constantly gives me an error. Please check the server logs. =)
August 22nd, 2008 at 11:00 pm
[…] functionality of abstract classes works properly in all concrete subclasses. Certainly this has been mentioned before, generally under the name Abstract Tests or Contract Tests. Also, the idea seems […]