mocking

Avoiding Brittle Tests with Mockito’s ArgumentCaptor

Thursday, June 10th, 2010 | Uncategorized | 1 Comment

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:

interface Mailer {
  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

Welcome $name,

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 Mailer mailer
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.

Welcome $name,

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.

@Test
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.

Gotta catch 'em all!

The ArgumentCaptor is a specialised ArgumentMatcher that records the matched argument for later inspection.

@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
  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.

Tags: , , , ,

BDDMockito & Eclipse

Sunday, August 9th, 2009 | Uncategorized | 4 Comments

On the 23rd of July, Mockito 1.8.0 was released, you can see a full listing of changes in their release notes. One feature that took my fancy was the inclusion of BDD aliases for stubbing an API, instead of using when, the Mockito team now encourage the use of given to bring your tests inline with the BDD style. To illustrate this, here is an example taken from the BDDMockito javadoc.

import static org.mockito.BDDMockito.*;
 
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
 
public void shouldBuyBread() throws Exception {
  //given  
  given(seller.askForBread()).willReturn(new Bread());
 
  //when
  Goods goods = shop.buyBread();
   
  //then
  assertThat(goods, containBread());
}

By breaking the test into given, when & then, future maintainers of the tests will more quickly understand which parts of the test relate to setup, exercising the SUT and asserting.

(stealing from Apple’s AppStore ad) What’s great about Eclipse, is that if you want to write a foreach loop, there’s a template for that. If you want to create a unit test, there’s a template for that. Yip there’s an app template for just about anything.

To minimise keystrokes and encourage the use of this style, you can modify the existing Test template as shown below to automatically generate the BDD style comments & statically import the BDDMockito members. To modify the template, navigate to Window > Preferences > Java > Editor > Templates. Click on Test (the JUnit 4 test template) and click edit. Paste in the follow template:

@${testType:newType(org.junit.Test)}

public void ${testname}() throws Exception {
  // given ${cursor}
  ${staticImport:importStatic('org.junit.Assert.*', 'org.mockito.BDDMockito.*')}

  // when

  // then

}

You are now ready to start writing BDDMockito style unit tests using Eclipse’s template, simply type Test, hit Ctrl+Space and you will be given the following option.

ctrl-space

Selecting the option will generate the test method, focus first given to the test name, then to the blank line after // given.

created-template

There you have it, a simple Eclipse template to generate your BDDMockito style tests.

Tags: , , , , , , ,