Unit Testing Complex Java Objects with Mockito
I’m sure this scenario has happened to you: Out of the corner of your eye you notice your team’s QA engineer approaching with a confused look on her face. Suddenly, your heart starts to beat faster…
Most of you would start to dread the upcoming conversation. “What did I do wrong? It was working for me, I swear!” you think to yourself.
As she gets closer you start to become a little more honest with yourself: “Well, I guess I didn’t fully test all the use cases but she must know that I was on a tight deadline and testing takes forever.”
Let’s imagine a different scenario: She reaches your desk and says “All good! That new testing tool is paying dividends.”
You spend the rest of the day not being afraid of your team’s QA because you realize the impact that rapidly creating tests is having on your confidence to ship high quality code.
Reasons for testing usually fall into three categories:
- You’re doing what you’re told
- You’re super responsible
- You (like me) find that seeing a line of green “test-passed” circles in Android Studio incredibly satisfying
Testing simple objects that do not require a lot of dependencies is usually straightforward. As our objects become more and more complex they typically will require more and more dependencies to construct. This creates two issues (1) construction of the object we are testing can be unnecessarily complex and time consuming and (2) failures in our tests can be caused by poor implementation from some other object that we injected.
Mockito allows us to control the behaviour of injected objects, allowing us to isolate the object we are testing form implementation errors caused by other objects. Mockito does this by providing developers with tools like Mock, When, Verify, Captor and Spy. In this blog post I’m going to show us how writing tests with Mockito leads to code that is more meaningful, easier to build and understand. We will look at a simple example and then show a more complex example to demonstrate how more complex objects can maintain its simplicity with the use of Mockito.
Consider a basic example, where we are testing a class Owlet. In this example we’ll call our class “Owlet”. Owlet is just a small simple owl that doesn’t do anything other than provide it’s age when asked. This class takes in a primitive when it is constructed.
This object is straightforward and doesn’t need much to construct. Give the age of the Owlet and we are free to use this object as we like.
In many cases, the object that we are testing isn’t so simple and requires more to construct. Let’s take our Owlet example,which has grown up from a young owlet and is now a complicated teenage Owl.
When confronted with an object such as our new Owl, there are two things we could try:
- Pass null objects for Nest, Career and OwlHobby: Null objects can be passed in situations where the nulled object is only used in areas outside the scope of our tests. Quite often this does not work since we will need to call methods of the injected objects. In this case OwlHobby cannot be null, so this option is out.
- Construct a new object for each parameter: This option adds unnecessary complexity and can be incredibly time consuming. The new objects we create to pass to Owl may be even more complex than Owl itself and so require even more new objects to be created. This process can be time consuming for us to write and creates a needless amount of code, so this option is not ideal.
MockitoMockito allows us to easily create objects that aren’t null but don’t need to be passed any parameters in order to be constructed. By default the mocked objects return null for all of its methods. These mocked objects can then be injected into the object we are testing, making it super easy to setup the object we intend to test.
Here’s how to create a Mocked object:
- Declare the Mock objects in the test class
- Initialize the Mocked in our setup method
- Construct the object we are testing with the new Mocked objects
That’s it! Now we have a new Owl object and can test it accordingly. If you tried this and your code compiled but caused an error when it ran don’t panic, below are a few tools that will help you out.
Note, private or final objects cannot be mocked.
Generally speaking, the reason why we need to inject things into our object is because they use these dependencies to do call methods on our injected objects. If our mocked object returns null for everything it can cause some runtime errors. Using when allows us to set what the return will be if a certain method is called.
Instead of null, our mocked object mMockCareer will return true if increaseSalary() is called with the parameter 1000 and false when it is called with 0. If it is called with anything other than 0 or 1000 then it will return null.
If we want it to return true regardless of what integer is passed to it then instead of 1000 use anyInt() to declare this:
Being able to control the return of these methods is great for two reasons:
- When our Owl object constructs it may need to call a method from one of the injected objects. In this case, it will throw an error if it returns either null or something it doesn’t expect. Using when lets us get around this obstacle and we can just tell it to return the value it expects when the given method is called.
- This allows us to isolate the tests for Owl from implementation errors in other objects. If mMockCareer.increaseSalary() should return true if we passed it the integer 1000 but it returns false instead, it wouldn’t be immediately obvious that our test failed because Career’s implementation. Our first assumption would probably be that something is wrong with Owl and not Career.
What if our object’s method doesn’t return anything but instead performs logic that should call a function of another object? Because we are testing Owl we don’t need to test the logic for any other other object. All we should be testing here is that the proper method was called.
Without verify we would have to test the final output from the chain of commands that may depend on methods of other objects. The difficulty with this approach is that if the tests failed it would be unclear as to what object is responsible for the failure. Did our test fail because of a logic problem within the object we are testing, or was it because of an object we passed control to?
Verify lets us skip that and just test that the control was handed off to the proper object.
This test will pass at this point if the increaseSalary method has been called 3 times. We can confirm how many times it was called with a given parameter:
This test will pass if increaseSalary was called only once with the integer 1000.
Captor is another useful construct that lets us record the parameters that are passed to mocked object’s method. Take our increaseSalary example, suppose we want to verify what it is passed to that object– we can use a captor record all the integers that were passed to increaseSalary. Here’s how:
- Declare the captor at the top of our test class
- When using verify, instead of using anyInt() use the captor argument that we declared
- Use JUnit assert methods to confirm that the captor contains values we expect.
In this case we asserted that the method increaseSalary() was called with the integer 1000 and not with the integer 3000.
Sometimes we may have to create a non-mocked new object in order to run our tests properly. But what about all our cool stuff, like verify and when, can we not use those anymore? With Spy objects we can do just that. A spied object acts exactly like an object normally would– but we can now use verify and when.
Here’s how we use a Spy object:
- Declare it at the top of our test class
- Initialize the objects in our startup method
- Use it just as we would a mocked object
As mentioned a Spy object will act like it would in the wild, unless one of it’s method is overridden with when/doReturn . In contrast, a Mock object will by default just return null for any method that is called.
ConclusionAt the beginning of the post we had two main concerns with testing complex objects:
- construction of our object can be ugly and time consuming
- failures in the test can be caused by poor implementation from an object we inject.
We used verify and captor to avoid relying on Nest, Career and OwlHobby’s return when testing Owl methods that didn’t have a return. By doing this we further isolated Owl from outside errors. Finally we learned how to use Spy, which extends the benefits from Mock, while allowing the spied object to act normally.
Overall, when I write tests with Mockito I find the code more meaningful, easier and quicker to build, understand, and debug.
About the Author
Trevor is a Co-op Software Developer, working on the Android app with the Publishing team. He has a Bachelor of Commerce and is finishing up his Bachelor of Computer Science at UBC. In his spare time you can catch him in Whistler, an ice rink, or a boxing gym.