July 3, 2012

Mockito: Default values and how to get more out of them

The problem

A while ago I had an interesting discussion with two of my colleagues, Dan Bergh Johnsson and Daniel Deogun, about how to change the default return value of unstubbed methods in Mockito. The background for the discussion was that by default, Mockito returns default values for unstubbed methods and these default values are usually good enough. In some cases that default value will be null and if the code being tested tries to use the returned null value it will cause a nullpointer exception. The stack trace of that exception will quite easily give away that the cause was an unstubbed method.

However, if the tested code calls an unstubbed method and, instead of using the null value directly, stores it and uses it later. Then the stack trace of the nullpointer exception will no longer reveal that the cause was an unstubbed method but will point to some random code line that can be far away from where the unstubbed call actually took place. When this happens it usually takes some time to figure out what the root cause was. Lets take a look at how we would approach this situation and how we can get a more informative error when it occurs.


The solutions

The first thing you should do is to stop, and think about the code you are testing. The fact that it is making calls to unstubbed methods, i.e. methods that you did not intend or expect the code to call, should make you suspicious. You should start off by asking yourself questions like: Is your code overly complex and doing more than one thing? Does it have too many, or unexpected, dependencies? Or, as my colleague Daniel Deogun put it, why is there a dependency that you don't want your test code to reflect?

Maybe the most appropriate solution is to refactor your code rather than starting to tweak your test code.

But if you are working with legacy code, or if you are writing tests to code that has already been written and you simply do not have time to refactor it now, then there are ways to solve this with Mockito [1]. Lets take a look on how to do this.

One nice way to get a better clue from the stacktrace from the nullpointer exception is to use the RETURNS_SMART_NULLS answer in Mockito. (see the javadoc). You can specify the RETURNS_SMART_NULLS default answer when you are creating your mock like:
User userMock = mock(User.class, Mockito.RETURNS_SMART_NULLS);
When using RETURNS_SMART_NULLS the mock will return a SmartNull instead of a null. When your code tries to use the SmartNull value you will get a SmartNullPointerException instead of a nullpointer exception and the stack trace will show you where the call to the unstubbed method was made. This is really helpful compared to the previous staktrace that revealed nothing. You should also notice that RETURNS_SMART_NULLS will return null/SmartNull in fewer cases than default.

Ok, that second solution will help us avoid those awful nullpointer exceptions but you might still get some unwanted or unexpected results since there are a lot of cases where the default value is not null. Sometimes you may want a way to totally forbid calls to unstubbed methods and when such a call is made you want to throw an exception letting everybody know about the horrible violation. You can achieve this behavior by providing your own custom default answer.

A mock with such an exception-throwing answer would look like this:
User userMock = mock(User.class, new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        throw new MockitoException("Unstubbed method call");
    }
});
So what happens when you start writing your givens?
when(userMock.getAddress()).thenReturn(new Address("Duckburg"));
Or
given(userMock.getAddress()).willReturn(new Address("Duckburg"));
if you prefer the BDD style.
Well, you will get that "Unstubbed method call" stack trace from your default answer. Not exactly the result we wanted. It turns out that the method you are trying to stub actually gets called when you are stubbing the method.

- @#$&! catch-22 implementation of Mockito!
- We need to find a new mocking framework! Start googling! Quick!

Wait. Before you start whipping out your googling skills, take a deep breath and start surfing the documentation of Mockito instead, or just read this blog post, and we will find the third solution. It turns out the developers of Mockito have already thought of our problem. There is another way you can write your givens:
doReturn(new Address("Duckburg")).when(userMock).getAddress();
This way, the method getAddress() will not be called when you are stubbing it to return an actual value.

To me, this syntax feels very backwards and does not read as well as using the when(..).then(..) syntax and, though I know about it, I hardly ever uses it. And you shouldn't either since the when(..).then(..) syntax is the preferred and recommended way to use Mockito. But this "reversed" notation can indeed be a handy trick when you actually need it. And now you know it too so you can take advantage of it, and you also know that you should use it sparingly.


The afterthought

As I mentioned in the beginning of this post, the need for doing more complex stubbing can often be an indicator of deficiencies in how your code is designed. It is the symptom of a root cause. And, as many experienced developers know and have learned the hard way, it is always better to solve the root cause of a problem instead of the symptoms because as long as the root cause exists, new symptoms will keep emerging.

Or, to sum it up, good code is easy to test. So if your code is hard to test...



------------
[1]: Don't worry, those who wrote the untested code in the first place will learn the hard way why you should not write the tests after the code. Right?
Btw, you are also putting up "improved test coverage on already written code" on the board so the team can deal with it next time they pull tasks. Writing tests to code not designed for testability is always a good way to learn why/how to design for testability but that is a topic that deserves its own post.

2 comments:

  1. Hi Daniel,

    Nice blog! Is there an email address I can contact you in private?

    ReplyDelete
    Replies
    1. Hi Nikos,

      thank you! Glad you liked it.
      I don't put up my email here because of spam but you can DM me on Twitter if you want.

      Delete