The use of TDD along with mock objects while designing for testability can help us to write extremely high quality code. In fact I have learned that practicing these techniques regularly can help move 100% coverage away from a nearly impossible (and questionably valuable) goal and much closer to a milestone achievable with an extra bit of incremental effort.
There are situations I have come across where the steps required for complete design for testability seem just a bit excessive. Sometimes when we design for testability we get application code and tests that are much more complex than we would like them to be.
Let’s look at an example.
public class ComplexValue { private String ourProperty; private String otherProperty; private String lastProperty; public ComplexValue() { ... } public ComplexValue(String ourProperty) { this.ourProperty = ourProperty; ... } public String getOurProperty() { return ourProperty; } }public interface ActionService { public void act(ComplexValue value); }public class Actor { private ActionService service; public Actor(ActionService service) { this.service = service; } public void doAction(String propertyValue) { service.act(new ComplexValue(propertyValue)); } }
Notice that ComplexValue has three property fields. In this context assume we care about one (the one that we construct it with) and the other two properties may or may not be given other values in the constructor.
The simplest test we can write for this class uses jMock like this:
public class ActorTest {
private Mockery mockery = new Mockery();
private ActionService actionService;
private Actor actionClass;
@Before
public void setUp() {
actionService = mockery.mock(ActionService.class);
actionClass = new Actor(actionService);
}
@Test
public void testDoAction() {
mockery.checking(new Expectations() {
{
one(actionService).act(with(any(ComplexValue.class)));
}
});
actionClass.doAction("Test Property Value.");
mockery.assertIsSatisfied();
}
}
This type of test will do wonders for your coverage report. It’s not really testing much though is it? The responsibility of the Actor is to construct the ComplexValue with the property we passed in and pass that along to the ActionService. When we ignore the value of the property in the test we in turn ignore a large part of the Actor functionality.
If we redesign slightly for testability we can write much better tests.
public interface ComplexValueFactory { public ComplexValue build(String property); }public class ComplexValueFactoryImpl implements ComplexValueFactory { public ComplexValue build(String property) { return new ComplexValue(property); } }public class Actor { private ActionService service; private ComplexValueFactory factory; public Actor(ActionService service, ComplexValueFactory factory) { this.service = service; this.factory = factory; } public void doAction(String propertyValue) { service.act(factory.build(propertyValue)); } }
Now the tests will look like this:
public class ActorTest { private Mockery mockery = new Mockery(); private ActionService actionService; private ComplexValueFactory factory; private Actor actionClass; @Before public void setUp() { actionService = mockery.mock(ActionService.class); factory = mockery.mock(ComplexValueFactory.class); actionClass = new Actor(actionService, factory); } @Test public void testDoAction() { final ComplexValue dummyValue = new ComplexValue(); mockery.checking(new Expectations() { { one(factory).build("Test Property Value."); will(returnValue(dummyValue)); one(actionService).act(dummyValue); } }); actionClass.doAction("Test Property Value."); mockery.assertIsSatisfied(); } }public class ComplexValueFactoryImplTest { private ComplexValueFactoryImpl factory; @Before public void setUp() { factory = new ComplexValueFactoryImpl(); } @Test public void testBuild() { String propertyValue = "Property Value For Test"; ComplexValue expectedValue = new ComplexValue(propertyValue); assertEquals(expectedValue, factory.build(propertyValue)); } }
Now that we have moved the construction of the ComplexValue into a separate factory we can test that Actor appropriately calls that factory and uses the result as we expect it to. Independently, we can also test that the factory produces the ComplexValue as expected. While this fulfills our goals of useful tests and in fact introduces a factory which might someday be useful for other reasons it still seems like a lot of extra clutter in our application code. Lets explore another option that can get us the same valuable testing without so much load on our application code.
Lets take a look again at the original test that we wrote for Actor:
public class ActorTest {
private Mockery mockery = new Mockery();
private ActionService actionService;
private Actor actionClass;
@Before
public void setUp() {
actionService = mockery.mock(ActionService.class);
actionClass = new Actor(actionService);
}
@Test
public void testDoAction() {
mockery.checking(new Expectations() {
{
one(actionService).act(with(any(ComplexValue.class)));
}
});
actionClass.doAction("Test Property Value.");
mockery.assertIsSatisfied();
}
}
In this test we used the jMock “any” matcher to insure that the ActionService’s act() method with an instance of ComplexValue. In order to make this test more useful we would like to insure the the act() method is called with an instance of ComplexValue that with the ourProperty property matching the value that we specified to the Actor. We can write just such a test if we create a custom Hamcrest matcher for jMock to validate against.
public class ComplexValueOurPropertyMatcher extends TypeSafeMatcher<ComplexValue> {
private String ourPropertyValue;
public StringStartsWithMatcher(String ourPropertyValue) {
this.ourPropertyValue = ourPropertyValue;
}
public boolean matchesSafely(ComplexValue v) {
return v.getOurProperty() == ourPropertyValue;
}
public StringBuffer describeTo(Description description) {
return description.appendText("a ComplexValue with ourPropertyEqual to ").appendValue(ourPropertyValue);
}
}
This is a custom Hamcrest matcher that will return true to the matchesSafely method if it is passed a ComplexValue that has the same ourProperty value as the one specified. In addition we will create a matcher factory that will make the jMock expectations using our new matcher easier to read.
@Factory
public static Matcher<ComplexValue> complexValueWithOurProperty(String ourProperty ) {
return new ComplexValueOurPropertyMatcher(ourProperty);
}
Now we can modify our test class to look like this:
public class ActorTest {
private Mockery mockery = new Mockery();
private ActionService actionService;
private Actor actionClass;
@Before
public void setUp() {
actionService = mockery.mock(ActionService.class);
actionClass = new Actor(actionService);
}
@Test
public void testDoAction() {
mockery.checking(new Expectations() {
{
one(actionService).act(with(complexValueWithOurProperty("Test Property Value.")));
}
});
actionClass.doAction("Test Property Value.");
mockery.assertIsSatisfied();
}
}
Now we have a test that is just as valuable as the tests we were able to write with the Factory solution while not needing to add any additional application code.
In many cases a solution that is designed for testability is the best way to create good tests. Applications that are designed for testability often get maintainability, extensibility, and simplicity as side effects. It can still be very useful to have the additional flexibility that using Hamcrest custom matchers in your mock object tests can provide.
- Michael Feathers’ Blog - The Flawed Theory Behind Unit Testing
- you’ve been HAACKED - What Integrated Circuits Say About Testing Your Code
Post a Comment