mockito
Constructor injection, and how it simplifies unit test setup
I’ve recently been reading Growing Object-Oriented Software Guided by Tests (GOOS), and one (of the many) aha moments was a piece of test code that mocked the collaborators and instantiated the object under test – all in the declaration of the test’s private fields. I am particularly fond of this approach for two reasons:
- The test code setup is minimal and easily scanned
- This approach encourages all required collaborators to be passed in through the constructor (aka constructor injection)
I’ve included an illustrative example below using Mockito, the actual test isn’t important but it proves this setup style works.
import org.junit.Test;
public class ItemCheckerTest {
private final ItemFetcher itemFetcher = mock(ItemFetcher.class);
private final Notifier notifier = mock(Notifier.class);
private final ItemChecker itemChecker = new ItemChecker(itemFetcher, notifier);
@Test
public void notifiesStoreManager() throws Exception {
given(itemFetcher.fetch()).willReturn(new FetchedItem());
itemChecker.check();
verify(notifier).notifyStoreManager();
}
...
}
For those unfamiliar with Mockito, the given call stubs a query, while the verify call uses a test spy to check a command call was made.
The important lines are the 3 private member variables of the test class, the first 2 use Mockito’s mock method to instantiate test doubles for our collaborators. The 3rd member variable (itemChecker) is the object under test, you will notice that it is instantiated with both of its required collaborators in the constructor. These 3 lines perform all the wiring we require for our test, without having to resort to @Before methods to set properties.
The reason we can leverage the member variables for this setup is that JUnit creates a new instance of ItemCheckerTest for each of the test methods (@Test). Providing each test with its own set of collaborators ensuring each test runs in isolation.
The most important side effect of setting up the test code in this fashion is that it promotes the use of constructors for wiring up collaborators. Using the constructor for collaborators has a couple of very appealing aspects:
- It becomes impossible to create circular dependencies between your objects
- Your objects are less prone to wiring bugs as they are upfront about their required collaborators.
Why would you want to be upfront about your collaborators, Steve Freeman & Nat Price (GOOS) have this to say:
Partially creating an object and then finishing it off by setting properties is brittle because the programmer has to remember to set all the dependencies. When the object changes to add new dependencies, the existing client code will still compile even though it no longer constructs a valid instance. At best this will cause a NullPointerException, at worst it will fail misleadingly.
Miško Hevery also has a great blog post on constructor vs setter injection.
Avoiding Brittle Tests with Mockito’s ArgumentCaptor
One of the core principles behind my love of Mockito is its ability to avoid brittle tests, by brittle tests I mean unit tests which fail when seemingly unrelated functionality changes.
Below I will outline one of Mockito’s lesser known features, the ArgumentCaptor that shines in certain use cases.
A common requirement for a web application is to send emails to users. In this case our web application is a travel booking system. If you make use of a templating language such as Velocity your email generation service might look similar to the following:
void send(
String to,
String templateName,
Map<String, Object> model)
}
Note: All examples are shown in Groovy for brevity, but the examples apply perfectly well to Java.
Imagine the email we send is simply welcoming the user to our web site upon registration
Thanks for signing up!
Book your holiday at http://example.com/
As you can see the only dynamic element required in the model map is the user’s name. A unit test to define the behaviour of our registration service will simply verify that a map is passed with the user’s first name.
private RegistrationService registrationService
@Before
void setUp() {
// Inject the mocked mailer
mailer = mock(Mailer)
registrationService = new RegistrationService(mailer)
}
@Test
void shouldSendRegistrationEmailWelcomingUser() {
// given a new user
User user = new User("Jim", "jim@example.com")
// when the user registers
registrationService.register(user)
// then an email should be sent welcoming the user
verify(mailer).send("jim@example.com", "welcome.vm", ["name":"Jim"])
}
As you can see the test starts off simple, we are verifying the mailer’s send call was invoked with the correct email address, template name and model data. In this case the model simply contains the name of the newly registered user.
A new requirement arrives which states we want to include the latest holiday deals in the welcome email.
Thanks for signing up!
Book your holiday at http://example.com/
Check out our latest travel offerings:
#foreach($offer in $offers)
<!-- Print out the offer details -->
#end
In order to test this requirement, a naive approach would be to simply add the travel offering assertions into the first test we created. This has the downside that the test is no longer specific to a particular requirement, as we add more content to our email the test would continue to grow and become unwieldy (especially if any conditional logic exists). We want to keep our tests focussed by limiting each test to a single logical assert.
Below we create a second test specific to the inclusion of the latest offers.
void shouldSendRegistrationEmailWithLatestTravelOfferings() {
// given a new user
User user = new User("Jim", "jim@example.com")
// and the latest travel offers
def offers = ["Offer 1", "Offer 2"]
given(latestOffers.get()).willReturn(offers)
// when the user registers
registrationService.register(user)
// then an email should be sent with the latest travel offers
verify(mailer).send("jim@example.com", "welcome.vm", ["offers":offers])
}
At first glance this looks like a good test, we are checking the latest offers are included in the model so they can be rendered in the email template.
Unfortunately both tests will fail.
In order to verify the correct calls are made, Mockito uses the equality (equals) method of the passed arguments. In the above case we are checking the equality of two strings and a map. It is of course the equality of the model map that is causing the test to fail.
shouldSendRegistrationEmailWelcomingUser() fails as Mockito is expecting a map containing simply the user’s name [name:"Jim"], but due to the added travel offerings the map is actually [name:"Jim", offers:offers]. The same failure applies to shouldSendRegistrationEmailWithLatestTravelOfferings() as it is verifying the map only contains offers.
As mailer.send() has no return type (void) we have no simple way to access the model map in our test. However Mockito offers a couple of ways around this, the first is the creation of a custom Matcher. The second is to use an ArgumentCaptor which is the approach I will be using today.
The ArgumentCaptor is a specialised ArgumentMatcher that records the matched argument for later inspection.
void shouldSendRegistrationEmailWelcomingUser() {
// given a new user
User user = new User("Jim", "jim@example.com")
// when the user registers
registrationService.register(user)
// then an email should be sent welcoming the user
ArgumentCaptor<Map> model = ArgumentCaptor.forClass(Map)
verify(mailer).send(eq("jim@example.com"), eq("welcome.vm"), model.capture())
assertThat(model.value.name, is("Jim"))
}
Firstly an ArgumentCaptor is created for the class, in this case Map, we wish to inspect. The ArgumentCaptor is then used as an ArgumentMatcher in the verify call. No matter what keys or values the model map contains, the ArgumentCaptor will always match thus allowing the verify call to succeed. Now that we have captured the model we can inspect it by calling getValue() (simply value in Groovy) to access the original map that was passed to the Mailer service by our production code. By verifying only the key/value pairs that are specific to our test (the user’s name) we can ensure that any other pieces of information that are added to the map in the future don’t affect any of the existing tests.
The astute reader may have noticed the inclusion of equality matchers for both the email address eq("jim@example.com") and the template name. While Mockito relies on the equality method of the arguments by default, if any of the arguments are an ArgumentMatcher, then all of the arguments must be a matcher.
In summary, the ArgumentCaptor allows tests to remain focussed with a single logical assert, even when the object you wish to inspect is created in the code under test and passed to a collaborator via a void method call.
Subscribe
Recent Posts
Tags
- "Sorry, no matches found for “tdd” near Auckland, New Zealand" http://t.co/Ahc736MX #auckland #whowantstostartonewithme 2 weeks ago
- Great series on the SOLID design principles for JavaScript http://t.co/ELoPuOH2 2 weeks ago
- Auckland devs, what meetups should I be attending? #java #groovy #ruby #javascript #bdd #tdd 2 weeks ago
- More updates...
