Wednesday, September 7, 2011

Obstacles to Learning JMockit

JMockit employs some very strange tricks to keep your mock object setup short and declarative. The following list of strangeness may inoculate you against complete bafflement when you try to learn from the tutorials:

First, an overview of basic terminology and concepts so you can understand the weirdness:

  • JMockit uses a record-replay-verify model in which mock object behavior is "recorded" in an "expectations block", then test code is run (replaying the expected behavior), then mock object interactions are verified in a "verifications block".
  • JMockit is primarily used to mock object types (Interface or Class), not object instances. In the example below, any invocation of the method "mockMethod()" on an instance of "Dependency" will return the mock result "someValueOrException" regardless of the instance invoked! This is handy if your "objectUnderTest" does not provide a way to set a mock object instance.
  • Any type (not instance) of a field/parameter declaration preceded by the @Mocked annotation (or similar annotation such as @NonStrict) or declared as a field in an expectations block will be mocked by JMockit.
  • An expectations block is used to declare the method calls you expect your unit test may invoke on mock objects and the return values (or exceptions thrown) that you want your unit test to get back from those invocations.
  • A verifications block is used to declare the method calls your unit test must (or must not) invoke.
   @Test
public void testOperationXyz()
{
...

new NonStrictExpectations() // an "expectation block"
{
Dependency mockInstance; // "Dependency" is our mocked type for this test
...

{
...
// "mockInstance" is a mocked instance automatically provided for use in the test
mockInstance.mockedMethod(...);
result = someValueOrException; // declare expected result of first invocation
...
}
};

...
MyUnitClass objectUnderTest = new MyUnitClass();
objectUnderTest.operationXyz(); // test invocation
...

new VerificationsInOrder() {{ // a "verification block"
...
mockInstance.mockedMethod(...);
times = 1; // declare some property of this method invocation to be verified
...
}};

// Additional verification code, if any, either here or before the verification block.
}

Now, here are the aspects of JMockit that I found very surprising:

    • The declaration of mock behavior is specified in an anonymous class constructor (new NonStrictExpectations() {...} for example), but the result of this constructor is not saved explicitly. The association of this behavior with the actual invocations of mock object methods at runtime occurs as a side effect!
    • The mock behavior is declared in an "instance initializer" block within the anonymous class constructor, but since no instance is created in your test code, the state being initialized is not really an instance in the normal sense.
    • The mock behavior declaration commonly consists of an invocation of a mocked object method followed by a "result = ;" statement. This is intended to be read as "recording" that when the mocked object method is invoked as indicated, the result returned should be the value of . HOWEVER, this is an overlaying of a new language semantics upon Java code. The actual Java semantics dictate that during construction of the anonymous class instance, the mocked object is invoked and then expression is assigned to the Expectation field "result". Since Java semantics dictate there is no shared state between the invocation and the assignment to "result", these statements could appear in any order with any other statements between, but JMockit "recording" semantics require that the assignment to "result" appear right after the invocation. A sequence of multiple "result = ..." statements after an invocation records the values to return (or exceptions to throw) for each of a sequence of matching invocations. How the execution of the anonymous class constructor achieves the intended semantic side effect of defining subsequent mock object behavior is a mystery. The key seems to be that JMockit modifies the class to be mocked at runtime by reflecting on the behavior declared in this instance initializer block. Note that the first statement in "result = x; result = y;" would be useless in any Java program normally, but JMockit reflects on this code to record a sequence of results to be returned on later invocations.
    • JMockit allows you to specify a mocked method invocation return value AND a thrown exception with the same "result = " syntax. If you need to specify that a Throwable is returned and not thrown, you have to use the alternate "returns()" method.
    • You may use annotations to declare mock objects, or not. When the object is declared in an expectation block, its type is mocked automatically, so the annotation is optional.
    • Mocked objects are implicitly instantiated; you don't need code to instantiate them. By default, even if you instantiate and initialize an instance of a mocked class outside of the JMockit specific code, you will get the mocked behavior, because JMockit has modified the bytecode of the class itself.
    • "JMockit does not care on which object a mocked instance method is called." The mocked behavior is shared by all mocked instances of a type so that JMockit can inject this mock behavior everywhere the type is used, even when the instance is constructed within the unit under test. If you want per-instance mocking, @Injectable gives us an "exclusive" mocked instance; other instances of the same type are not mocked.
    • How do you mock a constructor invocation? Invoke the constructor in an expectations block, followed by exceptions thrown, if any.
    • How do you mock other static methods on a class? Invoke the static methods in an expectations block.
    • Requires Java 6 (or Java 5 with a vm argument). Unit tests we write will not run in a Java 5 build environment without the vm argument!!!
    • @Cascading indicates that expectations for a cascade of invocations (e.g., mock.getChannel().isConnected()) can specify the result of the cascade without having to specify the result of each separate invocation. This is a nice feature.
    • Debugging a JMockit unit test can be very difficult, because the internals of how mock object results are returned are not well documented. For example, if arguments to a mock object invocation do not properly implement "equals(Object other)" then your invocation may not match any expected invocation and will return null instead of your intended result. It is very difficult to step through the mock object framework matching code to find the particular argument that is failing to match.

    You might ask yourself if JMockit is worth learning and using. I certainly did and my conclusion was that you should definitely use a simpler framework if your unit under test allows you to create and set the mock objects you need. For example, if the objects to be mocked all can be injected with setter methods and if all implement an Interface type that you can implement with a mock implementation, then you do not need any mock object framework. If you need the ability to mock static methods and inject mocks into objects under test that do not have setter methods, then JMockit is probably the simplest mock framework to use after you master its unusual API.

    The main alternative that I've heard of is PowerMock in combination with the EasyMock framework. This alternative offers essentially the same power with a more verbose, but natural syntax. However, I don't know that learning PowerMock/EasyMock is any easier for the cases where such power is needed, and the resulting code is probably more difficult to maintain, if only because it is more verbose.

    No comments:

    Post a Comment