Generic Custom Argument Matching in EasyMock

admin ·

EasyMock is a clean, simple library for creating mock objects. It provides a whole range of facilities for declaring our expectations of the method call interactions on the mock including call count, call order, return values, exceptions, and argument matchers. Unfortunately, the argument matchers provide a limited set of argument expectations and a somewhat cumbersome process for expanding that set. Using java generics, it is possible to create a custom argument matcher allowing simple expression of unlimited assertions about Object arguments.

Imagine we have a test fixture, BlogEntryController, which depends on a single service, BlogService. Because we want to focus our testing on the fixture, we’ll inject a mock object as the service.

//follow EasyMock's convention of static imports
import static org.easymock.EasyMock.*
...
BlogEntryController controller = new BlogEntryController();
BlogService mockBlogService = createMock(BlogService.class);
controller.setBlogService(mockBlogService);

The next step is to record the expectations about the interactions between the fixture and its collaborator. In the record phase, we simply interact with the mock to record these expectations:

//record phase
BlogEntry blogEntry = new BlogEntry();
mockBlogService.saveBlogEntry(blogEntry);

//replay phase
replay(mockBlogService);

//method under test
controller.handleRequest(request, response);

//verify
verify(mockBlogService);

By default, argument expectations are verified using the equals() method. For the previous expectation, when the controller code is run, EasyMock will assert that the BlogEntry passed in by the controller equals() that passed in during the record phase. If, for instance, the controller creates its own BlogEntry and BlogEntry has the default implementation of equals(), this expectation would fail. Fortunately, EasyMock provides other argument matchers which can verify arguments based on different criteria:

mockBlogService.saveBlogEntry(isA(BlogEntry.class));

By using the isA(Class clazz) argument matcher, one of several predefined in EasyMock, the code will verify that the argument passed from the controller to the mock service is an instance of the BlogEntry class. By relaxing our expectations, we’ve come up with a test case which would pass. However, generally speaking, we’ll probably want to test some more details about the BlogEntry. For instance, it’s common to want to verify the argument’s properties.

EasyMock has the facility to create custom argument matchers. It’s a somewhat heavy weight solution requiring the implementation of 3 methods: 2 for the IArgumentMatcher interface and one static method used during the record phase. This facility seems better suited to creating reusable argument matchers (akin to the predefined ones which come with EasyMock) rather than singular matchers relevant only to a particular test case.

To bridge the gap, I have created a generic argument matcher, EasyMockUtils.argAssert(Assertion<E> assertion), which abstracts away the required EasyMock interaction code, allowing one to more simply express, inline if desired, the argument expectations:

static import common.test.easymock.EasyMockUtils;
...
mockBlogService.saveBlogEntry(argAssert(new Assertion() {
    void check(BlogEntry blogEntry) {
        assertEquals("submitted blog entry body", blogEntry.getBody());
        assertNull(blogEntry.getDateCreated());
    })

The argAssert static method takes a genericized instance of the Assertion interface in which one can declare any number of assertions against the argument passed in during the replay phase. As an added benefit, the Assertion implementation could also be leveraged to modify the passed in argument. For instance, imagine the controller depends on the service setting the dateCreated property on the passed in bean. We can ensure our mock replicates that behavior:

mockBlogService.saveBlogEntry(argAssert(new Assertion() {
    void check(BlogEntry blogEntry) {
        assertEquals("submitted blog entry body", blogEntry.getBody());
        assertNull(blogEntry.getDateCreated());
        //mimic the behavior of the true service
        blogEntry.setDateCreated(new Date());
    })

Mocking to this detail could trigger a warning regarding the level of coupling between the test, the fixture, and the collaborator; however, I have seen situations where it can make the difference between being able to use a mock object for testing or not. Mockist testers in general must be wary of unnecessarily deep coupling where, for instance, a refactor of the fixture/collaborator interactions could result in as much time spent updating test code even if the test’s high level assertions remain unchanged. However, used attentively during test driven development, mock objects can help a programmer focus on keeping those interactions simple and clearly defined. Furthermore, testing with mock objects helps one to focus on unit testing the fixture at hand. In that situation, the generic custom argument matcher provides a simplified tool for expressing assertions about that code.

And here’s the code. The generic interface which allows one to specify argument assertions:

Assertion.java

package common.test.easymock;

public interface Assertion
{
    /**
    * Allows specifying varying assertions about an argument.
    * @param argument the argument being check
    * @throws Error throw an exception in the case of an assertion failure
    */
    void check(E argument) throws Error, Exception;
}

A simple container for the static method used to declare the argument matching assertion during the mock record phase.

EasyMockUtils.java

package common.test.easymock;

import org.easymock.EasyMock;

public class EasyMockUtils
{
    public static  E argAssert(Assertion assertion)
    {
        EasyMock.reportMatcher(new ArgumentAssertion(assertion));
        return null;
    }
}

And the IArgumentMatcher implementation which ties the pieces together:

ArgumentAssertion.java

package common.test.easymock;

import org.easymock.IArgumentMatcher;

public class ArgumentAssertion implements IArgumentMatcher {
    private Assertion assertion;
    private Error assertionError;

    public ArgumentAssertion(Assertion assertion) {
        this.assertion = assertion;
    }

    public boolean matches(Object actual) {
        try
        {
            assertion.check(actual);
            return true;
        }
        catch (Error e)
        {
            assertionError = e;
            return false;
        }
        catch (Exception e)
        {
            assertionError = new Error(e);
            return false;
        }
    }

    public void appendTo(StringBuffer buffer) {
        buffer.append("argumentAssertion(exception ").append(assertionError).append(")");
    }
}

And of course a test:

ArgumentAssertionTest.java

package common.test.easymock;

import static common.test.easymock.EasyMockUtils.argAssert;
import junit.framework.TestCase;
import static org.easymock.EasyMock.*;

public class ArgumentAssertionTest extends TestCase
{
    public interface TestInterface
    {
        void testMethod(String arg);
    }

    TestInterface mock;

    protected void setUp() throws Exception
    {
        super.setUp();
        mock = createMock(TestInterface.class);
    }

    public void testArgumentAssertionSucess()
    {
        //record expected behavior
        mock.testMethod(argAssert(new Assertion() {
            public void check(String argument)
            {
                //do nothing - should pass
            }
        }));

        //should get no errors
        replay(mock);
        mock.testMethod("test");
        verify(mock);
    }

    public void testArgumentAssertionFailure()
    {
        //record expected behavior
        mock.testMethod(argAssert(new Assertion() {
            public void check(String argument)
            {
                fail("explicitly fail assertion");
            }
        }));

        replay(mock);

        boolean testSucceeded = false;

        try
        {
            mock.testMethod("test");
            verify(mock);
            testSucceeded = true;
        }
        catch (AssertionError e)
        {
            //should get this error
        }

        if (testSucceeded)
            fail("should have gotten a failure");
    }
}