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.