Partial Mocking Using Mockito

Posted by coffeetechgaff on April 16, 2017

We can test a method by mocking another method of a class that is being tested. However, the method that we are going to mock should be public. We have to do this type of testing frequently in our TDD environement. In this blog, I will show, how we can test our method by mocking another method in a same class using Mockito.

Purpose

Unit testing a method by mocking another method in a same class.

Scenerio

I am writing unit tests for my Storm Topology. One of our requirement is to retrieve all storm configurations from Redis. Since, I have to instantiate my Redis connection inside the main method, I have to use PowerMockito to create new mocked object inside the method.

Whenever I use PowerMockito to instantiate object, SonarQube won't generate coverage on that test (It is a bug in SonarQube). I ended up splitting into mulitple methods to bring up the SonarQube coverage by moving object instanciation to another method.

Dependency

                      
                      
                          org.mockito
                          mockito-all
                          1.10.19
                      
                    

Implementation

Class that needs to be tests which holds the multiple methods. I wanted to test loadTopologyProperties method without depending on Redis instanciation. So, I wanted to mock getRedis method. getRedis method will instantiate dao class to retrieve the topology properties.

                      package directory.topology;

                      import java.util.Arrays;
                      import java.util.HashMap;
                      import java.util.Map;

                      import org.apache.commons.lang3.ArrayUtils;
                      import org.apache.storm.Config;
                      import org.apache.storm.StormSubmitter;
                      import org.apache.storm.generated.AlreadyAliveException;
                      import org.apache.storm.generated.AuthorizationException;
                      import org.apache.storm.generated.InvalidTopologyException;
                      import org.apache.storm.kafka.KafkaSpout;
                      import org.apache.storm.topology.BoltDeclarer;
                      import org.apache.storm.topology.InputDeclarer;
                      import org.apache.storm.topology.TopologyBuilder;
                      import org.slf4j.Logger;
                      import org.slf4j.LoggerFactory;

                      /**
                       * Topology that connects all spouts and bolts and executes in a sequential
                       * manner.
                       *
                       * @author VivekSubedi
                       *
                       */
                      public class DirectoryTopology{

                      	private static final Logger logger = LoggerFactory.getLogger(DirectoryTopology.class);

                      	/**
                      	* omitted all the value intanciation for testing purpose
                      	*/

                      	/**
                      	 * This method creates a topology and submits to the cluster either to local
                      	 * or remote depending on the provided parameters
                      	 *
                      	 * @param args
                      	 *            - Command Line arguments
                      	 * @throws AuthorizationException
                      	 * @throws InvalidTopologyException
                      	 * @throws AlreadyAliveException
                      	 * @throws Exception
                      	 * @AlreadAliveException @InvalidTopologyException @AuthorizationException
                      	 */
                      	protected void submitTopology(String[] args) throws AlreadyAliveException, InvalidTopologyException,
                      			AuthorizationException{

                      		if(ArrayUtils.isEmpty(args)){
                      			logger.error("Command line argument can not be null or empty");
                      			throw new IllegalArgumentException("Command line argument cannot be null or empty");
                      		}

                      		if(args.length != 4){
                      			logger.info("Argumements provided by user are:");
                      			Arrays.asList(args).forEach(logger::info);
                      			logger.error("Number of argument should be exactly four arguments in following order \n1. Topology Name \n2. RedisIP \n3. RedisPort \n4. RedisKey");
                      			throw new IllegalArgumentException(
                      					"\nNumber of argument should be exactly four arguments in following order \n1. Topology Name \n2. RedisIP \n3. RedisPort \n4. RedisKey");
                      		}
                      		Arrays.asList(args).forEach(logger::info);
                      		logger.info("Reading Topology properties from redis");
                      		Config config = loadTopologyProperties(args);
                      		logger.info("Configuration has been loaded from readis {}", config);

                      		/**
                      		* omitted the topology builder for testing purpose
                      		*/

                      		/**
                      		 * submitting storm topology to cluster
                      		 */
                      		String topologName = args[0];
                      		config.setNumWorkers(numbWorkers);
                      		logger.info("Submitting topology [{}]", topologName);
                      		StormSubmitter.submitTopology(topologName, config, topologyBuilder.createTopology());
                      		logger.info("Topology has been submitted to remote cluster successfully!");

                      	}

                      	/**
                      	 * Reading all configurations of directory topology from Redis and loaded to
                      	 * to @Config storm object
                      	 *
                      	 * @param args
                      	 *            -Arguments provided by user from command line
                      	 * @return @Config object
                      	 */
                      	public Config loadTopologyProperties(String[] args){
                      		// parsing the command line arguments
                      		String redisIp = args[1];
                      		int redisPort = Integer.parseInt(args[2]);
                      		String redisKey = args[3];

                      		// creating @Config object and loading that object from redis
                      		Config config = new Config();
                      		Redis redis = getRedis(redisIp, redisPort);

                      		// loading each topology value to the config
                      		DirectoryUtils.getTopologyList().forEach(i -> {
                      			if(redis.exists(redisKey, i)){
                      				config.put(i, redis.getProperty(redisKey, i));
                      			}
                      		});

                      		// loading default config values for storm
                      		config.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, 2048);
                      		config.put(Config.TOPOLOGY_BACKPRESSURE_ENABLE, false);
                      		config.put(Config.TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE, 16384);
                      		config.put(Config.TOPOLOGY_EXECUTOR_SEND_BUFFER_SIZE, 16384);

                      		return config;
                      	}

                      	public Redis getRedis(String redisIp, int redisPort){
                      		RedisClient redisClient = new RedisClient(redisIp, redisPort);
                      		return new Redis(redisClient);
                      	}

                      	/**
                      	 * Main method to kick off the topology
                      	 *
                      	 * @param args
                      	 *            - command line arguments
                      	 * @throws Exception
                      	 * @AlreadAliveException @InvalidTopologyException @AuthorizationException
                      	 */
                      	public static void main(String[] args) throws Exception{
                      		DirectoryTopology topology = new DirectoryTopology();
                      		topology.submitTopology(args);
                      	}
                      }
                    

Testing above class’s loadTopologyProperties method by mocking getRedis method as follows:

                      package directory.topology;

                      import static org.junit.Assert.assertEquals;
                      import static org.junit.Assert.assertNotNull;
                      import static org.mockito.Mockito.spy;
                      import static org.mockito.Mockito.when;

                      import org.apache.storm.Config;
                      import org.junit.AfterClass;
                      import org.junit.Before;
                      import org.junit.BeforeClass;
                      import org.junit.Test;
                      import org.junit.runner.RunWith;
                      import org.mockito.Answers;
                      import org.mockito.InjectMocks;
                      import org.mockito.Mock;
                      import org.mockito.MockitoAnnotations;
                      import org.mockito.Spy;
                      import org.slf4j.Logger;
                      import org.slf4j.LoggerFactory;

                      /**
                       *
                       * @author VivekSubedi
                       *
                       */
                      public class DirectoryTopologyTest{

                      	private static final Logger logger = LoggerFactory.getLogger(DirectoryTopologyTest.class);


                      	@BeforeClass
                      	public static void setUpBeforeClass() throws Exception{
                      		logger.info("============ START UNIT TEST ==============");
                      	}

                      	@AfterClass
                      	public static void tearDownAfterClass() throws Exception{
                      		logger.info("============ END UNIT TEST ==============");
                      	}

                      	@Before
                      	public void setUp() throws Exception{
                      		MockitoAnnotations.initMocks(this);
                      	}

                      	@Test
                      	public void testLoadTopologyProperties(){
                      		logger.info("Running testLoadTopologyProperties...");

                      		// loading data
                      		String[] args = new String[]{"topology", "192.168.99.100", "9999", "directory"};
                      		String redisIp = args[1];
                      		int redisPort = Integer.parseInt(args[2]);
                      		String redisKey = args[3];
                      		String datasourceTopic = "datasource.topic";
                      		String analyticTopic = "analytic.topic";

                      		// mocking
                      		DirectoryTopology spyTopology = spy(DirectoryTopology.class);
                      		Redis redis = mock(Redis.class);
                          //whenever we mock another method of a same class, we need to use doReturn()
                      		doReturn(redis).when(spyTopology).getRedis(redisIp, redisPort);
                      		when(redis.exists(redisKey, datasourceTopic)).thenReturn(true);
                      		when(redis.exists(redisKey, analyticTopic)).thenReturn(true);
                      		when(redis.getProperty(redisKey, datasourceTopic)).thenReturn("datasources");
                      		when(redis.getProperty(redisKey, analyticTopic)).thenReturn("avroNode");

                      		// calling real method
                      		Config config = spyTopology.loadTopologyProperties(args);

                      		// veryfing the call
                      		logger.info(config.toString());
                      		assertNotNull(config);
                      		assertEquals(6, config.entrySet().size());
                      		assertEquals("datasources", config.get(datasourceTopic));
                      		assertEquals("avroNode", config.get(analyticTopic));
                      	}
                      }
                      

We need to annotate @Spy the class that we are testing i.e. DirectoryTopology. Line 63 to 66, we are mocking the expected output of another method of mocked class. Everything else is self explanatory and very easy to understand.