EasyMock: Capturing arguments from multiple calls

The EasyMock mocking framework makes it easy to use mocks in tests, to set up precise expectations to mock method calls, and to simulate return values and exceptions thrown from mock methods.

However, in some cases it is difficult to set up sufficiently precise expectations for the mocks

  • because we don’t know in advance with which arguments they get called or how often they get called
  • or because the verification, we have to make in the test, involves relating the calls to one mock object to the calls to another mock object or relating calls to mock objects to data returned from the object being tested.

In such cases the solution may be to capture “traces” of the method calls to the mock objects, and then, after having exercised the object being tested, to make the needed verifications using the captured traces.

Example

Consider as an example a card game simulation with a Dealer object collaborating with multiple Player objects:

public interface Dealer {
    public void deal(Set deck);
    public void addPlayer(Player player);
}

public interface Player {
    public void addCard(Card card);
}

The rules for the Dealer are:

  • Each player must get at least two, and at most six cards
  • Four cards for each player must be dealt in total
  • Cards cannot disappear, ie. after dealing, any card not given to a player must remain in the deck
  • Cards dealt to the players must be removed from the deck
  • The dealer may deal the cards randomly

Naive test

Now suppose we want to test the Dealer using EasyMock to mock the Players. How can we set up the expectations to test that the Dealer follows the rules? We can expect that the Dealer will call Player.addCard(), but we can’t assume in advance exactly how often and with which cards. We may set up expectations like this:

@Test
public void testDealFor2Players()
{
    Player player1Mock = createMock(Player.class);
    player1Mock.addCard( (Card) anyObject() );
    expectLastCall().times(2, 6);

    Player player2Mock = createMock(Player.class);
    player2Mock.addCard( (Card) anyObject() );
    expectLastCall().times(2, 6);

    replay(player1Mock, player2Mock);

    Dealer dealer = new DealerImpl();
    dealer.addPlayer(player1Mock);
    dealer.addPlayer(player2Mock);

    Set deck = createDeck();
    dealer.deal(deck);

    verify(player1Mock, player2Mock);
}

With this naive test we only get to test the rule “Each player must get at least two, and at most six cards” – this is clearly not by any measure a good enough test of the Dealer.

In order to test the other rules, we have to somehow relate the actual calls to player1Mock.addCard() to the actual calls to player1Mock.addCard() and to the state of the deck after executing Dealer.deal().

Simple capture

EasyMock has a feature called Capture which allows us to capture the arguments with which mocks are called, such that we can verify the argument values after having exercised the object being tested. It works something like this:

Player player1Mock = createMock(Player.class);
Capture capturedCard = new Capture();
// Expect one call to addCard with any arg. Capture arg value for later
player1Mock.addCard( capture(capturedCard) );

// ...

dealer.deal(deck);

// Now we can verify what we want about the captured card, eg.
assertEquals( Card.Suit.SPADES, capturedCard.getValue().getSuit() );

This seems like a step in the right direction. However, using this capture feature is hard when we have to capture multiple values from multiple calls to the mock objects, and doesn’t really seem to work at all when we don’t know the number of calls in advance.

Capturing values from multiple calls

The solution to the problem lies in writing our own EasyMock argument matcher, similar to how EasyMock implements capture internally, but extended so it can accept multiple argument values for multiple methods calls:

  • Create a class MultiCaptureMatcher implementing org.easymock.IArgumentMatcher. The class has a collection instance variable to collect the argument values. EasyMock delegates to an instance of this class whenever the corresponding mock object method gets called.
  • Create a static method to be used when setting up expectations – this static method creates an instance of the MultiCaptureMatcher and tells EasyMock to use that matcher for the correponding expected method call.

Here is the code:

public class MultiCaptureMatcher implements IArgumentMatcher {

    Collection captureDestination;

    public MultiCaptureMatcher(Collection captureDestination) {
        this.captureDestination = captureDestination;
    }

    @Override
    public void appendTo(StringBuffer buffer) {
        buffer.append("multiCapture(").append(captureDestination.toString()).append(")");
    }

    @Override
    public boolean matches(Object actual) {
        captureDestination.add((T) actual);
        return true;
    }

    public static  S multiCapture(Collection destination) {
      reportMatcher(new MultiCaptureMatcher(destination));
      return null;
  }
}

With this argument matcher we can now set up “expectations” like this:

Player player1Mock = createMock(Player.class);
Set player1Cards = new HashSet();
player1Mock.addCard( MultiCaptureMatcher.multiCapture(player1Cards) );
expectLastCall().anyTimes();

I put “expectations” in quotation marks because it is not really an expectation in any real sense, but rather tells EasyMock, that the addCard method may get called any number of times with any arguments – EasyMock shouldn’t do any verification, but just capture the arguments for later manual verification.

Here the player1Cards set acts as a destination for captured values. Every time during the test that player1Mock.addCard() gets called, the argument gets added to the player1Cards set.

After the Dealer has been tested, we can then use the captured values in player1Cards to whatever verifications we may want.

Here is a complete example using MultiCaptureMatcher to test all of the Dealer rules:

@Test
public void testDealFor2Players()
{
    Player player1Mock = createMock(Player.class);
    Set player1Cards = new HashSet();
    player1Mock.addCard( MultiCaptureMatcher.multiCapture(player1Cards) );
    expectLastCall().anyTimes();

    Player player2Mock = createMock(Player.class);
    Set player2Cards = new HashSet();
    player2Mock.addCard( MultiCaptureMatcher.multiCapture(player2Cards) );
    expectLastCall().anyTimes();

    replay(player1Mock, player2Mock);

    Dealer dealer = new DealerImpl();
    dealer.addPlayer(player1Mock);
    dealer.addPlayer(player2Mock);

    Set deck = createDeck();
    // Store original deck for later comparison
    Set originalDeck = new HashSet(deck);
    dealer.deal(deck);

    verify(player1Mock, player2Mock);

    // Each player must have been dealt 2 to 6 cards
    assertTrue(player1Cards.size()>=2);
    assertTrue(player1Cards.size()<=6);
    assertTrue(player2Cards.size()>=2);
    assertTrue(player2Cards.size()<=6);

    // For two players 8 cards must have been dealt
    assertEquals(8, player1Cards.size()+player2Cards.size());

    // Check that no cards have disappeared, and that any card is either in
    // the deck or belongs to one but not both players
    assertEquals(originalDeck.size(), deck.size()+player1Cards.size()+player2Cards.size());
    assertEquals(originalDeck, SetUtils.union(deck, player1Cards, player2Cards));
}

Caveat

There is one catch to be aware of with this approach: When an argument object is captured, we capture a reference to the object - not a copy of the objects state. This means that if argument objects get modified after being captured, for example because the object being tested modifies the argument objects after calling the mocks, then the verification of captured objects will see the modified object state instead of the object state at the time of capture.

This Post Has 3 Comments

  1. Very nice! I’m looking forward to reading this article.

  2. Nice and interesting, though I’d prefer to see the explaining comments to be put in the assert* calls instead:
    // For two players 8 cards must have been dealt
    assertEquals(8, player1Cards.size()+player2Cards.size());
    vs
    assertEquals(“For two players, 8 cards must have been dealt”, 8, …);

  3. EasyMock 2.5 has added support for capturing multiple values.
    See the Capture class and the CaptureType enumeration in the EasyMock 2.5 API.

Leave a Reply