jMock: Yoga for Your Unit Tests

Brittle tests are a common "gotcha" of test driven development. A brittle unit test will stop passing when you make unrelated changes to your application code. A test suite that contains a lot of brittle tests will slow down development and inhibit refactoring. A brittle test overspecifies the behaviour of the unit being tested. You can keep your tests flexible by following a simple rule of thumb: specify exactly what you want to happen and no more. This article describes how various features of jMock can help you strike the right balance between an accurate specification of a unit's required behaviour and a flexible test that allows easy evolution of the code base.

Stubs and Expectations

jMock distinguishes between stubbed and expected methods. A stubbed method call can occur during a test run, but if it does not occur the test still passes. An expected method call, on the other hand, must occur during a test run; if it does not occur, the test fails when it verifies the mock. When setting up your mocks you must choose whether to stub or expect each invocation. In general, we have found that tests are kept flexible when we follow this rule of thumb: stub queries and expect commands, where a query is a method with no side effects that does nothing but query the state of an object and a command is a method with side effects that may, or may not, return a result. Of course, this rule does not hold all the time, but it's a useful starting point.

A stub can be called any number of times. The number of times a method is expected is defined by the expectation itself. The once() expectation allows the method to be called once only; subsequent calls will make the test fail. The atLeastOnce() expectation allows the method to be called any number of times. The test will only fail if the method is never called at all. You can define further expectation types if these are not sufficient for your needs.

In the following example, the getLoggingLevel is stubbed: it does not have to be called. The setLoggingLevel must be called once and once only.

logger.stubs().method("getLoggingLevel").noParams()
    .will(returnValue(Logger.WARNING));
logger.expects(once()).method("setLoggingLevel").with(eq(newLevel));

Parameter Constraints

jMock requires that you specify a constraint on each actual parameter of a expected invocation, rather than specify just the expected parameter value. Specifying equality to an expected value is the most common case, but is too strict for many scenarios. For example, consider a system that logs errors to a Logger object. To test that an object correctly detects and logs an error, such as a failed attempt to connect to a database, you could set up an expectation on a mock logger to check the value of the message passed to the logger:

String expectedErrorMessage = "unable to connect to ORDERS database: network timeout";

logger.expects(once()).method("error").with(eq(expectedErrorMessage));

This will work in the short term, but will cause problems long term because it too precisely specifies the expected value of the error message, including minor details of punctuation and whitespace. If you change that formatting later, the test will fail even though the code under test still does what you want it to do. You really only care that the error message contains the information you want to report to the user — the action that failed and the cause of the failure — not how that information is formatted. You can use the constraint functions defined by the MockObjectTestCase class to specify exactly what you want to happen:

String action = "connect to ORDERS database";
String cause = "network timeout";

logger.expects(once()).method("error")
    .with( and(stringContains(action),stringContains(cause)) );

The MockObjectTestCase class defines several functions that can be used to define constraints. There are more constraint types defined in the org.jmock.constraint1 package. You can even define your own constraints2.

Constraints require you to type a little more when writing your tests, and require you to think more carefully about what behaviour you expect, but the result is that your tests are easier to read because they clearly express your intent and more flexible because they don't overspecify expected behaviour.

Invocation Order

By default, jMock allows invocations upon a mock object to occur in any order. Sometimes, however, the order of calls is important, such as when you are testing an object that fires events or writes data to an output stream. In such cases you can explicitly specify that a call should occur after one or more others.

logger.expects(once()).method("setLoggingLevel").with(eq(Logger.WARNING))
    .id("warning level set");
logger.expects(once()).method("warn").with(warningMessage)
    .after("warning level set");
logger.stubs().method("getLoggingLevel").noParams()
    .after("warning level set")
    .will(returnValue(Logger.WARNING));

A rule of thumb to follow when specifying the expected order of method calls is: test the ordering of only those calls you want to occur in order. The example above allows the warn and getLoggingLevel methods to occur in any order, as long as they occur after the call to setLoggingLevel. Thus we can change the order in which our tested object calls warn and getLoggingLevel without breaking our tests.

Specifying the order of calls is orthogonal to whether those calls are expectations or stubs. So, the example above specifies that the getLoggingLevel method does not have to be called, but if it is, it must be called after setLoggingLevel, and that the warn method must be called and must be called after setLoggingLevel.

Matchers

The methods method(methodName), with(argument constraints) and after(prior call ID) are syntactic sugar that define matching rules that match an incoming invocation to an object that can mock the behaviour of the invocation and test its expectations. If jMock does not have a matching rule that you need, or the matching rules supported by jMock are not accurate enough to keep your tests flexible, you can extend jMock with matching rules of your own.

If you need more control over how method names are matched, you can specify a constraint over names instead of a precise name value. An invocation will match if the constraint evaluates to true when applied to the method name. Suppose that you want to stub all calls to the property getter methods of a mock object by synthesising a default result based on the type of the property. You could achieve with a constraint that matches method names that begin with "get" and that have no parameters. (See Writing Custom Constraints2 for the definition of the StringStartsWith class).

...

private DefaultResultStub returnADefaultValue = new DefaultResultStub();

public void testMethod() {
    ...
    mock.stubs().method(startingWith("get")).noParams().will(returnADefaultValue);
    ...
}

private Constraint startingWith( String prefix ) {
    return new StringStartsWith(prefix);
}

...

This example is, however, still brittle. You want the mock to stub all property getter methods but that is not actually what the test specifies. To precisely specify the required behaviour you can write your own matching rule3 that uses the Java Bean API to test if a method is a property getter.

...

private DefaultResultStub returnADefaultValue = new DefaultResultStub();

public void testMethod() {
    ...
    mock.stubs().match(aBeanPropertyGetter()).will(returnADefaultValue);
    ...
}

private InvocationMatcher aBeanPropertyGetter() {
    return new BeanPropertyGetterMatcher();
}

...

Conclusion

This article should have given you some idea of why the jMock API is designed as it is and how that API can help you avoid brittle tests. The most important rule of thumb we follow to keep our tests flexible is:

Specify exactly what you want to happen and no more.

This guideline is just as applicable when writing unit tests that do not use mock objects.

Nat Pryce

Document History