Monday, May 10, 2010

To Mock or not to Mock, that is the question...

Yoda: … But beware the mock side… Easy they flow, quick to join you in a test. If once you start down the mock path, forever will it dominate your destiny, consume you it will...
Luke: … Is the mock side stronger?
Yoda: No, no, no. Quicker, easier, more seductive.
Luke: But how am I to know the good mocks from the bad?
Yoda: You will know…

Over the last three years that I've been on a project doing extensive unit testing (not that I was ever doing unit testing before, though I'd heard the name) one of the major battles our team has had is about using mocks. Primarily it's "do you mock concrete classes or not". Mocking interfaces isn't such a question because that's the whole point of an interface, you write whatever implementation you want, and in a unit test that could very well be a mock object.

Mocking collaborators is a great way to work out the interactions between objects, and for stateless service methods allows you to isolate your tests to primarily just the object you're trying to test without by not forcing you to set up way too much data and classes and state around the other objects.

And of course, you have to think about the purpose of testing. Some say it's about using tests to design your objects. I don't disagree, but based on pretty long experience writing software, I think another purpose of tests is to "protect" your code from future changes.

We all know that code changes as the product/project or whatever evolves and goes through future changes, that's pretty much a given. Having spent years working on untested code, I like the idea that tests tell you later when changes affect code by failing. They tell you that a future change, possible to some other class, requires you to change a class you may not have thought of. It's great, and it helps reduce regression bugs later on.

At first it wasn't a question, we used JMock version 1 and that only allowed mocking interfaces. Using it we had test code that looked like:
Mock mockService = new Mock(BlahBlahBlahService.class);
mock.expects(once()).method("doSomething").with(eq(domainObjectId), eq(newValueAmount));

And so forth. It works, but the part that's inelegant is having the method name in quotes so that refactors don't automatically change it and you can't easily find where that method is called.

But then with JMock2, we got better syntax like:
BlahBlahBlahService mockService = createMock(BlahBlahBlahService.class);
addExpectations(new Expectations() { {
one(mockService).doSomething(domainObjectId, newValueAmount);
} });

But with this JMock2 also brought the ability to mock concrete classes... And that's where I think the trouble begins.

On our project we've set up "builders" for just about every domain object so that in tests we can fairly easily create a stateful domain object with any state our tests need. It works quite slick in our test code:
DomainObject domainObject = new DomainObjectBuilder()
.someStringValue("xyz")
.someOtherStringValue("abc")
.someObjectValue(new Object())
.someIntegerValue(123)
.build();

Granted, some can get fairly complicated with a test requiring building multiple objects. I'm not saying it's always 100% easy. And often if setting up data for your unit test is really hard, it's a sign your design isn't quite right and it's time to think about changing that. On the other hand, I kind of think that if the code is complicated, the test should be equally so, they go hand in hand, and if the test is too much simpler, it may not be a good test.

When we switched to JMock2, a lot of the younger, less experienced guys, who didn't have much experience at maintaining older code through generations of changes wanted to start mocking every object except the one being tested. They were concerned about "purity" and that every unit test had to be a "pure unit test" which meant that the object being tested was the only one not a mock. They wanted all other objects mocked so that each test tested only one single object.

I understand the argument about purity, but I disagree, if they're too pure, they're not future-proofing your code (in metallurgy, pure steel isn't as flexible if impurities, such as manganese aren't added). By mocking everything else, it prevents our tests from catching necessary changes in an object if the objects it collaborates with change in some way. The test is essentially "putting words in the mouth" of other objects. I think if an object is dependent upon another object, it's good for the test to be equally dependent.

Another coworker gave a good argument against "purity". How would you test this function?

public String truncateTo10Chars(String source) {
if (source.length() > 12) {
return source.substring(1, 12);
} else {
return source;
}
}

A mock test for that would be simply silly, mocking a String? It would look like:

String mockString = createMock(String.class);
String truncatedString = "123456789012";

addExpectations(new Expectations() { {
one(mockString).length()
will(returnValue(15));
one(mockString).substring(with(equalTo(1)), with(equalTo(12)));
will(returnValue(truncatedString));
} });

String actualString = truncateTo10Chars(mockString);
assertThat(actualString, is(truncatedString));

That'd be a bit of a crazy test...

One of the unit tests that a mock fan wrote in our code dealt with a domain object's method returning a particular enum value. The mock fans didn't want to build it, they wanted the test to be pure and the test of the class calling the domain object to mock the domain object to avoid "testing" the domain object itself. I tried to suggest "well, what if in the future it returns a different one of the enum's values, we'll have to change the code calling it because it's dependent upon what's returned, but the test won't tell us because it's still the same data type and it won't fail."

I lost the argument, not by logic but by not talking loud enough, not yelling my answer to "that'll never happen, this is the way we've written," and then the other mock fellows switching to Hindi to cut me out of it.

And two years later, we added a new option to the enum and changed the return values of newly created domain objects that used it. All the tests passed, and the code failed when we ran the application, because the tests used mocks and didn't tell us what code depended upon that domain object that needed changing.

Anyway, after quite a while I began to think a little different, and come to a balance. I saw an example of a concrete class where we didn't care what values were in it, we only wanted to make sure a particular method was called, and there a mock made sense.

So, what I've come to is, mocking interfaces is fine. But with concrete classes, never mock the state of it, but if your test has nothing to do with the state, it's okay. Essentially, that's mocking just the interaction, and that's essentially the concrete class's "interface".

No comments:

Post a Comment