I learn all sorts of things from
jMock. From jMock 1, I was infected with the idea of a fluent interface for creating mocks, and the power of Constraint objects. Lately, I've been playing with jMock 2, and it's had me thinking even more deeply about a pattern I'm for the moment calling
interface imposterization.*
To explain imposterization, I'd like to make a distinction between two kinds of subtypes of a given type (for this discussion, let's consider implementations of a Java interface). An
implementor of an interface satisfies all of the documented and implied contracts for that interface. An
impostor of an interface is also an implementation, as far as the object runtime is concerned, but is free to violate the interface contracts, in order to learn or assert or learn a property about the code that uses it.
If you've run across
mock objects in unit testing, then you've already seen one instance of imposterization. Consider constructing a mock for a BankAccount interface, in order to test a woefully primitive transaction processor, using jMock 1 syntax:
interface BankAccount {
void deposit(int amount);
void withdraw(int amount);
int getBalance();
}
@Test public void readDeposit() {
BankAccount account = mock(BankAccount.class);
account.expects(once()).method("deposit").with(eq(1000));
TransactionReader reader = new TransactionReader(account);
reader.readLine("deposit 1000");
}
Here,
account is an impostor of the BankAccount interface. It's not really a proper implementor, because there are all kinds of contracts, perhaps documented, and perhaps implied, that our mock BankAccount breaks. For one thing, it probably throws an expectation exception when
getBalance() is called, something that no proper implementation of BankAccount would do. But we're not trying to create a general-purpose implementation--the point of our imposterization is to learn something about the
readLine method: does it interact in the right way with
account?
jMock 2 also uses imposterization to set expectations on mock objects. Here's the same test in jMock 2 syntax:
@Test public void readDeposit() {
BankAccount account = mock(BankAccount.class);
expects(new InAnyOrder() {{
one(account).deposit(1000);
}});
TransactionReader reader = new TransactionReader(account);
reader.readLine("deposit 1000");
}
Here, the result of
one(account) is a different impostor, which records a method call (
.deposit(1000)) as a method call expected to be called later in the test.
one(account) doesn't make any pretense of being a real
BankAccount. From the point of view of the intent of object-oriented design, this is almost pure evil. But it is useful, because it reduces the "meta-noise" of the test. Rather than having to invent a new language to talk
about a method invocation, we can use regular Java to just invoke the method itself.**
Impostors are not a new idea to me--my thesis work on
test factoring involves creating "capturing decorators" to record invocations, and mocks to replay them--both kinds of impostors that we saw above. This is done automatically to create unit tests from arbitrary program executions, and run them after program changes, without the developer having to really know what's going on under the hood.
The new idea from jMock 2 is that plain-Java interfaces for creating impostors can be quite elegant and succinct. Knowing this, more and more problems begin to suggest imposterization to me. In my next post, I'll talk about using impostors to simplify testing exception-throwing methods, and later, we'll look at using impostors to automatically generate stubs for verifying
Theories.
* The name imposterization comes from the interface Imposteriser in jMock 2. However, I won't claim that the jMock authors had exactly the same idea of what imposterization means, and I'll just have to agree to disagree about the -ize vs. -ise ending, thanks to
Samuel Johnson.
** The idea of using an interface impostor to record mock object expectations has been around in EasyMock (and perhaps other frameworks) for a while. The benefits of this syntax are not without costs--I have always been uncomfortable with EasyMock's two-phase mock objects, which first capture and then replay, with a global method call in the middle to switch state. That smells to me of a missed abstraction. jMock 2 also, it turns out, uses two-phase mocks under the covers, but the interface at least encourages thinking about the recording phase differently, since developers call
one(account).deposit(1000) instead of directly
account.deposit(1000). But I still have the same concerns that something is being missed.