Unit testing is an essential part of any delivery; the logic contained in the bolts must also be unit tested.
Unit testing often involves a process called mocking that allows you to use dynamically generated fake instances of objects as dependencies in order to ensure that a particular class is tested on a unit basis. This book illustrates unit testing using JUnit 4 and JMock. Please take the time to read up on JMock's recipes online at http://jmock.org/cookbook.html.
In the
src/test/java
folder, create thestorm.cookbook
package and create theStormTestCase
class. This class is a simple abstraction of some of the initialization code:public class StormTestCase { protected Mockery context = new Mockery() {{ setImposteriser(ClassImposteriser.INSTANCE); }}; protected Tuple getTuple(){ final Tuple tuple = context.mock(Tuple.class); return tuple; } }
Then create the
TestRepeatVisitBolt
class that extends fromStormTestCase
, and mark it with the parameterized runner annotation:@RunWith(value = Parameterized.class) public class TestRepeatVisitBold extends StormTestCase {
The test case logic of the class is contained in a single
execute
method:public void testExecute(){ jedis = new Jedis("localhost",6379); RepeatVisitBolt bolt = new RepeatVisitBolt(); Map config = new HashMap(); config.put("redis-host", "localhost"); config.put("redis-port", "6379"); final OutputCollector collector = context.mock(OutputCollector.class); bolt.prepare(config, null, collector); assertEquals(true, bolt.isConnected()); final Tuple tuple = getTuple(); context.checking(new Expectations(){{ oneOf(tuple).getStringByField(Fields IP);will(returnValue(ip)); oneOf(tuple).getStringByField(Fields .CLIENT_KEY);will(returnValue(clientKey)); oneOf(tuple).getStringByField(Fields .URL);will(returnValue(url)); oneOf(collector).emit(new Values (clientKey, url, expected)); }}); bolt.execute(tuple); context.assertIsSatisfied(); if(jedis != null) jedis.disconnect(); }
Next, the parameters must be defined:
@Parameterized.Parameters public static Collection<Object[]> data() { Object[][] data = new Object[][] { { "192.168.33.100", "Client1", "myintranet.com", "false" }, { "192.168.33.100", "Client1", "myintranet.com", "false" }, { "192.168.33.101", "Client2", "myintranet1.com", "true" }, { "192.168.33.102", "Client3", "myintranet2.com", false"}}; return Arrays.asList(data); }
The base provisioning of the values must be done before the tests using Redis:
@BeforeClass public static void setupJedis(){ Jedis jedis = new Jedis("localhost",6379); jedis.flushDB(); Iterator<Object[]> it = data().iterator(); while(it.hasNext()){ Object[] values = it.next(); if(values[3].equals("false")){ String key = values[2] + ":" + values[1]; jedis.set(key, "visited");//unique, meaning it must exist } } }
Firstly, the unit test works by defining a set of test data. This allows us to test many different cases without unnecessary abstractions or duplication. Before the tests execute, the static data is populated into the Redis DB, thus allowing the tests to run deterministically. The test method is then executed once per line of parameterized data; many different cases are verified.
JMock provides mock instances of the collector and the tuples to be emitted by the bolt. The expected behavior is then defined in terms of these mocked objects and their interactions:
context.checking(new Expectations(){{oneOf(tuple).getStringByField(Fields.IP);will(returnValue(ip)); oneOf(tuple).getStringByField(Fields.CLIENT_KEY);will(returnValue(clientKey));oneOf(tuple).getStringByField(Fields.URL);will(returnValue(url)); oneOf(collector).emit(new Values(clientKey, url, expected)); }});
Although these are separate lines of code, within the bounds of the expectations they should be read declaratively. I expect the
getStringField
method of the tuple to be called exactly once with the value ip
, and the mock object must then return a value to the class being tested.
This mechanism provides a clean way to exercise the bolt.