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.