Mimicking External Actions With EasyMock

It will happen sometimes that a unit test will need to do some work that an external source might normally do. One easy-to-see example of this is an ID that gets generated by a database when an entity is persisted. It may be the case that the software will assume success, but it could (nay, should) also be the case that some validation or use of the identifier is done after the persistence, and this can be tricky when mocking these interactions.

Let’s take a really simple and dumb annotated Entity object.

@Entity
@Table(name="pojo")
public class POJO {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    public Long id;

    @Column
    public String something;
}

When used, we would expect the database to populate our id column when we saved the object for the first time. Simple, easy.

Let’s make a simple DAO definition to save such an object.

public interface POJODAO {
    public POJO saveOrUpdate(final POJO pojo);
}

We’ll leave the implementation of this up to the specific application, but now we know we have a way to save (or update) our POJO. Let’s make a trivial class that will do exactly this.

public class POJOWork {

    public POJODAO pojoDao = null;

    public void makeAndSavePOJO() {
        final POJO pojo = new POJO();
        pojo.something = Long.toHexString(System.currentTimeMillis());
        pojoDao.saveOrUpdate(pojo);
        if (pojo.id == null) {
            throw new RuntimeException("POJO Save Failed!");
        }
    }
}

Here we’ve got a pretty useless method that creates and saves one of our objects, assigning some goofy data to its member. Of course, in your code, this would be more useful. After the save is complete, the ID field is used. Here, for a trivial and probably very undesireable validation, the ID field is checked for null, which it shouldn’t be after a successful save, and will throw an Exception if the field is null. Again, of course, in your code something more useful would be done.

In good test-driven (or test-defended) development, we want to have a test that verifies our method does what we expect it to. There are a few obstacles in our method that we run into when we’re writing our test, the least of which is that the object we need to mock is local to the member.

Here’s a quick method that works around this, ensures that the DB interaction is mocked, and passes our simple method without hitting that Exception.

import static org.easymock.EasyMock.*;
import static org.junit.Assert.assertNotNull;

import org.easymock.IAnswer;
import org.junit.Test;

public class POJOWorkTest {
    @Test
    public void makeAndSavePOJOSuccess(){
        final POJOWork pojoWork = new POJOWork();
        pojoWork.pojoDao = createMock(POJODAO.class);
		
        expect(pojoWork.pojoDao.saveOrUpdate(isA(POJO.class))).andAnswer(new IAnswer() {
            @Override
            public POJO answer() throws Throwable {
                final POJO pojo = (POJO)getCurrentArguments()[0];
                assertNotNull(pojo.something);
                pojo.id = 0l;
                return pojo;
            }
        });
		
        replay(pojoWork.pojoDao);
        pojoWork.makeAndSavePOJO();
        verify(pojoWork.pojoDao);
    }
}

Digesting the test bit by bit, we start out making our object. We then assign its DAO to a mock implementation.

We know that we’re going to call the method, so we set up an expectation. Since we don’t have either an equals() method or other means by which to compare the object passed to our expectation, we simply accept one of its kind with the isA() EasyMock matcher. If EasyMock accepts our match (which it will), it then invokes our IAnswer, calling the answer() method.

In our answer() method, we grab the parameter (which we know from the matcher will be the right type and exist). We can do some validation here that helps us overcome other lack of access to the object; in our case, we just confirm that our POJO.somethign has a value.

Happy that our object is prepared for the DB, we play its part and assign the id field a value. Since our test simply makes sure it’s not null, we just give it a number. Should we have needed to, we could have put more thought into this, of course. Then, because our interface says we return our POJO, we do so from our answer() method.

With our peparation complete, the test tells EasyMock to start waiting for calls, we call the method, it runs, calling our prepared EasyMock method and running as if it were in a real execution flow, and finally we validate with EasyMock that our expectations were met. Running this test gives us total green-bar happiness.

Sure, that’s the easy one; a method with a return value. What happens if we don’t return anything, or what is returned has nothing to do with our object? Let’s change our interface a little to just save and not return our object.

public interface POJODAO {
    public void saveOrUpdate(final POJO pojo);
}

With that little change, our POJOWork code is still valid, as it was not making use of the returned value, but we can no longer use the easy expect().andAnswer() or expect().andReturn() EasyMock methods.

import static org.easymock.EasyMock.*;
import static org.junit.Assert.assertNotNull;

import org.easymock.IArgumentMatcher;
import org.junit.Test;

class POJOMatcher implements IArgumentMatcher {
    @Override
    public void appendTo(StringBuffer arg0) {
    }

    @Override
    public boolean matches(Object arg0) {
        final POJO pojo = (POJO) arg0;
        assertNotNull(pojo.something);
        pojo.id = 0l;
        return true;
    }
}

public class POJOWorkTest {
    private POJO isPOJO() {
        reportMatcher(new POJOMatcher());
        return new POJO();
    }

    @Test
    public void makeAndSavePOJOSuccess() {
        final POJOWork pojoWork = new POJOWork();
        pojoWork.pojoDao = createMock(POJODAO.class);

        pojoWork.pojoDao.saveOrUpdate(isPOJO());
        expectLastCall();

        replay(pojoWork.pojoDao);
        pojoWork.makeAndSavePOJO();
        verify(pojoWork.pojoDao);
    }
}

That’s a bit more work, and it’s not nearly as elegant or flexible. Let’s go over this bit-by-bit, too.

The new class, POJOMatcher, makes use of the EasyMock.IArgumentMatcher interface to allow EasyMock to send an object for comparison. The matches() method will be used to determine if that’s the right deal after all. Since we’ve really got nothing to compare to, we do our validation in the matches() method just like in the answer() method before, and when it passes we set the id. The argument passed to the matcher is the one created in our test method, so it will have an id when the method continues from the mocked save attempt.

Our test class has a new isPOJO() method, which kind of binds the IArgumentMatcher to our POJO type. While we do have to return an instance of our class, unless we want to use it to validate the information in the POJOMatcher.matches() method, it can be empty (as it is). had we wanted to, we could have made the isPOJO() more intelligent, perhaps taking a POJO as a parameter for comparison, and returning that after setting up the ReportMatcher.

In our test method, we changed the preparation a little bit. Since the method doesn’t return a value any more, we can’t pass it to EasyMock.expect(), and then can’t use the expect().andAnswer() or expect().andReturn() methods, which is how we got wrapped up in ReportMatcher and IArgumentMatcher anyway. “Calling” the method and then telling EasyMock we expect the last call using the expectLastCall() will satisfy the replay and verify and let the mock do its job.

When the call is run in the middle, our matcher will be called, comparing, ignoring our isPOJO()-provided POJO, and verifying the POJO actually passed in our implementation in the POJOMatcher.matches() method, and successfully setting the id.

Now, in both cases, we can validate the information contained in a local object when passing through a mocked method, and can also meet our initial goal of affecting the passed object as the real object is expected to have affected it.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*