Tuesday, July 19, 2011

TestNg example with jMock and reflection

   In this example, we'll see how to use reflection and jMock to unit test the code. Further the  following source uses a scheduler to execute a continuously task in a fixed interval.

   I have posted the source class below which is done using the spring framework. It's init method is to be called at the start up, which intern calls the execute method. The initial delay and the interval of the scheduler is obtained from another module and I do not want to mock the implementations of that. My objective is to directly test the private method (execute) in my test case.

==================================================================

package xxx.xxx.xxxxxxxx;

import xxx.xxxxxx.xxxx.xxxx.Message;
import xxx.xxx.xxxxxxxx.xxxxx.CurrentStatus;
import xxx.xxx.xxxxxxxx.xxxx.xxxxx.MessageRepository;
import xxx.xxx.xxxx.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static xxx.xxxxxx.xxxxx.RSR.sConfig;
import static xxx.xxxxxx.xxx.KKB.gDDK;
import static xxx.xxxxxx.xxx.KKB.DIK;


public class MessageScheduler {

    private int interval;
    private int initialDelay;
    private Service serviceEndpoint;
    private ScheduledExecutorService executorService;
    private MessageRepository messageRepository;
   
    private static final String RECORD_ID = "_XX";

    private static final Logger LOGGER = LoggerFactory.getLogger(MessageScheduler.class);

    public void init() {
        LOGGER.info("starting scheduler ....... ");
        interval = ((Long) sConfig().find(gDIK)).intValue();
        initialDelay = ((Long) sConfig().find(gDDK)).intValue();
        executeScheduler();
        LOGGER.info("started scheduler ....... ");
    }

    private void executeScheduler() {
        try {
            executorService.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    List<Map<String, Object>> messages = messageRepository.find(CurrentStatus.ALLOWED);
                    if(LOGGER.isDebugEnabled()) {
                        LOGGER.debug("number of messages to be sent: {}", messages.size());
                    }
                    for(Map<String, Object> msg : messages) {
                        Message message = new Message();
                        Object id = msg.remove(RECORD_ID);
                        message.putAll((Map) msg);
                        serviceEndpoint.sendService(message, null);
                        msg.put(RECORD_ID, id);
                        messageRepository.update(msg, CurrentStatus.SENT);
                    }
                }
            }, initialDelay, interval, TimeUnit.MINUTES);
        } catch(Exception e) {
            LOGGER.error("Message sending failed ... ", e);
        }
    }

    public void stop() {
        executorService.shutdown();
    }

    public void setExecutorService(ScheduledExecutorService executorService) {
        this.executorService = executorService;
    }

    public void setMessageRepository(
            MessageRepository messageRepository) {
        this.messageRepository =messageRepository;
    }

    public void setServiceEndpoint(Service serviceEndpoint) {
        this.serviceEndpoint = serviceEndpoint;
    }

    public void setInterval(int interval) {
        this.interval = interval;
    }

    public void setInitialDelay(int initialDelay) {
        this.initialDelay = initialDelay;
    }
   
}


======================================================================================================================================

In the test case I have mocked the Service and MessageRepository classes. Why should we mock such classes? It's because we shouldn't be testing the implementation of dependent classes in a test case, which is intended to test a particular class. In other words, a failure in one or more of the dependent classes shouldn't result a failure in the test case.

To walk through the TestNg annotations used here;
  • @BeforeMethod: Runs before each test method. (configuration needed for the tests can be done here)
  • @Test: Test methods. Methods annotated with this will be considered as a test method.
  • @AfterMethod: Runs after each test method. (normally used to release any resource used for testing)
Mockery of the jMock framework is the class used to mock (or fake) classes in the test case. Expectations are defined for the testing. Expectations are the expected method invocations and the results (in case the methods return something) of the classes, which are mocked.

Below is the TestNg test class for MessageScheduler. The explanations of jMock and reflection usages will follow.
==================================================================

package xxx.xxx.xxxxxxxx;

import xxx.xxxxxx.xxxx.xxxx.Message;
import xxx.xxx.xxxxxxxx.xxxxx.CurrentStatus;
import xxx.xxx.xxxxxxxx.xxxx.xxxxx.MessageRepository;
import xxx.xxx.xxxx.Service;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.concurrent.DeterministicScheduler;
import org.jmock.lib.legacy.ClassImposteriser;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.document.mongodb.MongoTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;


@ContextConfiguration(locations={"classpath : xxxx-xxxx.xml"})
public class GovernanceMessageSchedularTest {

    private MessageScheduler messageScheduler;
    private Mockery mockery;
    private MongoTemplate template;
    private DeterministicScheduler scheduler;
   
    @BeforeMethod
    public void setUp() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("xxxx-xxxx.xml");
        messageScheduler = (MessageScheduler) ac.getBean("messageScheduler");
        template = (MongoTemplate) ac.getBean("template");
        mockery = new Mockery() {
            {
                setImposteriser(ClassImposteriser.INSTANCE);
            }
        };
        final Service service = mockery.mock(Service.class);
        final MessageRepository messageRepository = mockery.mock(MessageRepository.class);
        scheduler = new DeterministicScheduler();
        Expectations expectations = new Expectations() {
            {
                exactly(2).of(service).sendService(with(any(Message.class)), with(aNull(HttpServletRequest.class)));
                oneOf(messageRepository).find(CurrentStatus.ALLOWED);
will(returnValue(getDummyList()));
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("_id", null);
                exactly(2).of(messageRepository).update(map, CurrentStatus.SENT);
            }

        };
        messageScheduler.setServiceEndpoint(service);
        messageScheduler.setMessageRepository(messageRepository);
        messageScheduler.setExecutorService(scheduler);
        messageScheduler.setInterval(1);
        mockery.checking(expectations);
    }
   
    @Test
    public void testSchedular() throws SecurityException, NoSuchMethodException,
                            IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
       
        Class<?> gms = MessageScheduler.getClass();
        Method executeSchedulerMethod =  gms.getDeclaredMethod("executeScheduler", new Class[]{});
        executeSchedulerMethod.setAccessible(true);
        executeSchedulerMethod.invoke(messageScheduler, new Object[]{});
        scheduler.tick(500, TimeUnit.MILLISECONDS);
        mockery.assertIsSatisfied();
    }
   
    private List<Map<String, Object>> getDummyList() {
        List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
        list.add(new HashMap<String, Object>());
        list.add(new HashMap<String, Object>());
        return list;
    }
   
    @AfterMethod
    public void tearDown() {
        messageScheduler = null;
    }
}
==================================================================

jMock usage break down

1. First step is to create an instance of Mockery;
mockery = new Mockery() {            {
                setImposteriser(ClassImposteriser.INSTANCE);
            }
        };
Here Imposteriser is set to ClassImposteriser in an initialization  block to enable mocking of classes. If not only interfaces will be mocked.

2. Required classes are mocked (initialized using mockery) next.

final Service servicem = mockery.mock(Service.class);
final MessageRepository messageRepository = mockery.mock(MessageRepository.class);

3. Then define expectations for the mockery. Expectations consists of method invocations and/or faked results of the mocked classes. The mocked methods are invoked inside the source class and will produce the results we fake.

Expectations expectations = new Expectations() {            {
                exactly(2).of(service).sendService(with(any(Message.class)), with(aNull(HttpServletRequest.class)));
                oneOf(messageRepository).find(CurrentStatus.ALLOWED);will(returnValue(getDummyList()));
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("_id", null);
                exactly(2).of(messageRepository).update(map, CurrentStatus.SENT);
            }

        };

In the above expectations we expect exactly 2 invocations of the method sendService of Service. The method will be passed any Message object and a null value as parameters.

exactly(2).of(service).sendService(with(any(Message.class)), with(aNull(HttpServletRequest.class)));

Further it contains an invocation of the method find with a parameter, ALLOWED of CurrentStatus, an enum. This invocation is expected only once and to return a list.

oneOf(messageRepository).find(CurrentStatus.ALLOWED);
will(returnValue(getDummyList()));

It also expects an invocation of update on messageRepository exactly twice with a given map and SENT of CurrentStatus as arguments.

4. Mocked methods are set to the instance with is being tested.

messageScheduler.setServiceEndpoint(service);
messageScheduler.setMessageRepository(messageRepository);

5. Then expectations are set to the mockery.


mockery.checking(expectations);

6. In the test case it can be tested if all the expectations  met. If not an assertion error will be thrown (test failure).

mockery.assertIsSatisfied();

7. The source class uses a ScheduledExecutorService. So all the invocations in the expectations will be done in separate threads. So the assertion will be failed, if this happens. jMock provides a DeterministicScheduler which implements the ScheduledExecutorService. We can use it to evade this problem.

scheduler = new DeterministicScheduler();
messageScheduler.setExecutorService(scheduler);


Further the schedulers interval is set in the set-up method to 1 minute.

we are able to move the time back and forth in the deterministic scheduler.
scheduler.tick(500, TimeUnit.MILLISECONDS);

So as per the test case when the scheduler passes 500 milliseconds, all the expectations of the mockery are expected to be satisfied.

Reflection usage break down

1. Get the class of the source.
Class<?> gms = MessageScheduler.getClass();

2. Get the private method declared as follows.
        Method executeSchedulerMethod = gms.getDeclaredMethod("executeScheduler", new Class[]{});

Here the method name is 'executeScheduler'. we pass an empty Class array since it does not take any arguments.

3. Make that method accessible.
        executeSchedulerMethod.setAccessible(true);

4. Invoke the method on the actual object (testing instance).
        executeSchedulerMethod.invoke(messageScheduler, new Object[]{});
Here an empty object array is passed as the second argument since the method does not take any arguments.

This way the private method on the source class can be accessed.


thanks,
Shyarmal.

No comments:

Post a Comment