Design by contract: testing implementations of interfaces
Here’s a nice way of associating contracts with interfaces and testing the implementations conform.
A sample interface:
``
interface CheeseMaker {
int getCheeseCount();
void addCheese(Cheese cheese);
}
With the contracts:
- there should be zero cheeses on
creation.
- adding a cheese should increment the
count.
- unless the cheese is a duplicate.
- when adding a cheese, the cheese cannot be null.
You can create an abstract unit test for this interface that tests these contracts. The only thing it doesn’t do is provide an implementation – instead it has an abstract factory method.
``
public abstract class CheeseMakerTest extends TestCase {
// abstract factory method
protected abstract CheeseMaker createCheeseMaker();
public void testZeroCheesesOnCreation() {
CheeseMaker cheeseMaker = createCheeseMaker();
assertEquals(0, cheeseMaker.getCheeseCount());
}
public void testAddingACheeseIncrementsCount() {
CheeseMaker cheeseMaker = createCheeseMaker();
cheeseMaker.addCheese(new Cheese("Cheddar"));
cheeseMaker.addCheese(new Cheese("Wensleydale"));
assertEquals(2, cheeseMaker.getCheeseCount());
}
public void testDuplicateCheesesDoNotIncrementCount() {
CheeseMaker cheeseMaker = createCheeseMaker();
cheeseMaker.addCheese(new Cheese("Cheddar"));
cheeseMaker.addCheese(new Cheese("Cheddar"));
assertEquals(1, cheeseMaker.getCheeseCount());
}
public void testNullCheeseCausesIllegalArgumentException() {
CheeseMaker cheeseMaker = createCheeseMaker();
try {
cheeseMaker.addCheese(null);
fail("expected exception");
} catch (IllegalArgumentException e) {} // good
}
}
Now, every time you create an implementation of CheeseMaker, the test should extend CheeseMakerTest and you inherit the contract tests for free.
For example:
``
public class BigCheeseMaker implements CheeseMaker {
// ... ommited for sanity
}
public class BigCheeseMakerTest extends CheeseMakerTest {
// factory method implementation
protected CheeseMaker createCheeseMaker() {
return new BigCheeseMaker();
}
// ... any additional tests go here
}
It’s important to note that this tests the contract but does not enforce them. It’s very flexible and there are very few contracts (if any at all) that couldn’t be expressed in a unit-test.
This helps defensive development with the added safety of unit-tests.
As a bonus, you can use TestDox to generate documentation for interfaces (much more useful than implementations), like so:
h4. CheeseMaker
- Zero cheeses on creation.
- Adding a cheese increments count.
- Duplicate cheeses do not increment
count.
- Null cheese causes illegal argument exception.