19 March 2012

Mockito is awesome!

Sometimes you need to test the "exceptional" behavior of your application just to make sure that an alternate path of your code, often needed to cope with an exception, perform as expected.

In these cases, writing a unit test is often complicated from the need to mimic some exception on external infrastructure services.

But wait! Mocking frameworks are made just for this... 

Ok let's keep in mind for now. Now imagine having to test the code that handle a specific exception of a jms messaging system.
The exception occurs infrequently so it is vain to hope of catching the case in an integration test, neither you can purposely provoke it.
All that remains is to simulate it on a unit test.

...It does not sound so simple. 

Wrong! With Mockito it's a breeze and with its fluent interface it's also funny!

Let's see how:

How to easily test spring jmstemplate

This is the method to test, a bit ugly, but self explaining:
public boolean send(final MyMessageInterface message) {
  int retryCount=0;
  int maxRetries=3;
  while (true) {
   try {
    jmsTemplate.execute(destination, new MyProducerCallback(message));
    return true;
   } catch (JmsException e) {
    if (++retryCount < maxRetries) {
     try {
      Thread.sleep(1000);
     } catch (InterruptedException e1) {
      // swallowed
     }
    } else if (e.getCause() instanceof some.vendor.CustomJmsException && failoverEnabled) {
     template.execute(fallbackDestination, new MyProducerCallback(message));
     return true;
    } else {
     throw e;
    }
   } 
  }
 }

And this is the test class:

import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
import javax.jms.BytesMessage;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.Session;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.jms.JmsException;
import org.springframework.jms.UncategorizedJmsException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.ProducerCallback;
import my.app.MyMessageInterface;
import some.vendor.CustomJmsException;
import some.vendor.CustomMessageProducer;

public class MySenderClassTest {

    MySenderClass testSubject;
    JmsTemplate mockTemplate;
    Session mockSession;
    CustomMessageProducer mockRemoteProducer;
    CustomMessageProducer mockLocalProducer;
    Queue mockQueue;
    Queue mockLocalQueue;
    BytesMessage mockMessage;

    @Before
    @SuppressWarnings("unchecked")
    public void setUp() throws Exception {
        testSubject = new MySenderClass();
        mockTemplate = mock(JmsTemplate.class);
        mockSession = mock(Session.class);
        mockRemoteProducer = mock(CustomMessageProducer.class);
        mockLocalProducer = mock(CustomMessageProducer.class);
        mockQueue = mock(Queue.class);
        mockLocalQueue = mock(Queue.class);
        mockMessage = mock(BytesMessage.class);
        when(mockTemplate.execute((Destination) anyObject(), (ProducerCallback<Void>) anyObject())).thenAnswer(
                new Answer<Void>() {

                    @Override
                    public Void answer(InvocationOnMock invocation) throws Throwable {
                        Object[] args = invocation.getArguments();
                        Destination dest = (Destination) args[0];
                        ProducerCallback<Void> pc = (ProducerCallback<Void>) args[1];
                        return pc.doInJms(mockSession, dest.equals(mockQueue) ? mockRemoteProducer : mockLocalProducer);
                    }
                });

        when(mockSession.createBytesMessage()).thenReturn(mockMessage);
        when(mockQueue.toString()).thenReturn("mocked distributed queue");
        when(mockLocalQueue.toString()).thenReturn("mocked local queue");
        testSubject.setDestination(mockQueue);
        testSubject.setFallbackDestination(mockLocalQueue);
        testSubject.setFailoverEnabled(true);
        testSubject.setJMSTemplate(mockTemplate);
    }

    @Test
    public void sendSuccessTest() throws Exception {
        testSubject.send(new MyTestMessage("my-valid-key"));
    }

    @Test
    public final void sendFailAfterThirdRetryTest() throws JMSException {
        doThrow(new UncategorizedJmsException(new CustomJmsException("mock exception"))).when(mockRemoteProducer).send((BytesMessage) anyObject());
        testSubject.send(new MyTestMessage("my-invalid-key"));
        verify(mockRemoteProducer, times(3)).send((Message) anyObject());
        verify(mockLocalProducer, times(1)).send((Message) anyObject());
    }

    public static final class MyTestMessage implements MyMessageInterface {
       //...
    }
}

Here you can find a very good presentation on Mockito and testing approaches by Per Lundholm.

Update 18/05/2012: Someone has rightly noted that the retries in the test subject, could be better addressed through a separate aspect.
Actually resorting to an aspect would help to clean up the code by separating the responsibility for sending to those of exception and subsequent retries handling,

In the past, I've used too AOP with mixed results and feelings.

While completely agreeing with the observation I prefer to keep as is the example code, though ugly and messed up, for lack of time and laziness mainly :), and to not complicate too much the example by introducing new topics.
As always, suggestions are welcome.