引言
Hadoop MapReduce作业有着独一无二的代码架构,这种代码架构拥有特定的模板和结构。这样的架构会给测试驱动开发和单元测试带来一些麻烦。这篇文章是运用MRUnit,Mockito和PowerMock的真实范例。我会介绍
1.使用MRUnit来编写Hadoop MapReduce应用程序的JUnit测试
2.使用PowerMock和Mockito模拟静态方法
3.模拟其他类型中的业务逻辑(译注:也就是编写测试驱动模块)
4.查看模拟的业务逻辑是否被调用(译注:测试驱动模块是否运行正常)
5.计数器
6.测试用例与log4j的集成
7.异常处理
本文的前提是读者应该已经熟悉JUnit 4的使用。
使用MRUnit可以把测试桩输入到mapper和/或reducer中,然后在JUnit环境中判断是否通过测试。这个过程和任何JUnit测试一样,你可以调试你的代码。MRUnit中的MapReduce
Driver可以测试一组Map/Reduce或者Combiner。 PipelineMapReduceDriver可以测试Map/Reduce作业工作流。目前,MRUnit还没有Partitioner对应的驱动。MRUnit使开发人员在面对Hadoop特殊的架构的时候也能进行TDD和轻量级的单元测试。
实例
下面的例子中,我们会处理一些用来构建地图的路面数据。输入的数据包括线性表面(表示道路)和交叉点(表示十字路口)。Mapper会处理每条路面数据并把它们写入HDFS文件系统,并舍弃诸如十字路口之类的非线性路面数据。我们还会统计并打印所有输入的非路面数据的数量。为了调试方便,我们也会额外打印路面数据的数量。
public class MergeAndSplineMapper extends Mapper<LongWritable, BytesWritable, LongWritable, BytesWritable> { private static Logger LOG = Logger.getLogger(MergeAndSplineMapper.class); enum SurfaceCounters { ROADS, NONLINEARS, UNKNOWN } @Override public void map(LongWritable key, BytesWritable value, Context context) throws IOException, InterruptedException { // A list of mixed surface types LinkSurfaceMap lsm = (LinkSurfaceMap) BytesConverter.bytesToObject(value.getBytes()); List<RoadSurface> mixedSurfaces = lsm.toSurfaceList(); for (RoadSurface surface : mixedSurfaces) { Long surfaceId = surface.getNumericId(); Enums.SurfaceType surfaceType = surface.getSurfaceType(); if ( surfaceType.equals(SurfaceType.INTERSECTION) ) { // Ignore non-linear surfaces. context.getCounter(SurfaceCounters.NONLINEARS).increment(1); continue; } else if ( ! surfaceType.equals(SurfaceType.ROAD) ) { // Ignore anything that wasn’t an INTERSECTION or ROAD, ie any future additions. context.getCounter(SurfaceCounters.UNKNOWN).increment(1); continue; } PopulatorPreprocessor.processLinearSurface(surface); // Write out the processed linear surface. lsm.setSurface(surface); context.write(new LongWritable(surfaceId), new BytesWritable(BytesConverter.objectToBytes(lsm))); if (LOG.isDebugEnabled()) { context.getCounter(SurfaceCounters.ROADS).increment(1); } } } } |
下面是单元测试代码,这段代码中用到了MRUnit,Mockito和PowerMock。
@RunWith(PowerMockRunner.class) @PrepareForTest(PopulatorPreprocessor.class) public class MergeAndSplineMapperTest { private MapDriver<LongWritable, BytesWritable, LongWritable, BytesWritable> mapDriver; @Before public void setUp() { MergeAndSplineMapper mapper = new MergeAndSplineMapper(); mapDriver = new MapDriver<LongWritable, BytesWritable, LongWritable, BytesWritable>(); mapDriver.setMapper(mapper); } @Test public void testMap_INTERSECTION() throws IOException { LinkSurfaceMap lsm = new LinkSurfaceMap(); RoadSurface rs = new RoadSurface(Enums.RoadType.INTERSECTION); byte[] lsmBytes = append(lsm, rs); PowerMockito.mockStatic(PopulatorPreprocessor.class); mapDriver.withInput(new LongWritable(1234567), new BytesWritable(lsmBytes)); mapDriver.runTest(); Assert.assertEquals("ROADS count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.ROADS).getValue()); Assert.assertEquals("NONLINEARS count incorrect.", 1, mapDriver.getCounters().findCounter(SurfaceCounters.NONLINEARS).getValue()); Assert.assertEquals("UNKNOWN count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.UNKNOWN).getValue()); PowerMockito.verifyStatic(Mockito.never()); PopulatorPreprocessor.processLinearSurface(rs); }
@Test
public void testMap_ROAD() throws IOException
{
LinkSurfaceMap lsm = new LinkSurfaceMap();
RoadSurface rs = new RoadSurface(Enums.RoadType.ROAD);
byte[] lsmBytes = append(lsm, rs);
// save logging level since we are modifying it.
Level originalLevel = Logger.getRootLogger().getLevel();
Logger.getRootLogger().setLevel(Level.DEBUG);
PowerMockito.mockStatic(PopulatorPreprocessor.class);
mapDriver.withInput(new LongWritable(1234567),
new BytesWritable(lsmBytes));
mapDriver.withOutput(new LongWritable(1000000),
new BytesWritable(lsmBytes));
mapDriver.runTest();
Assert.assertEquals("ROADS count incorrect.",
1,
mapDriver.getCounters().findCounter(SurfaceCounters.ROADS).getValue());
Assert.assertEquals("NONLINEARS count incorrect.",
0,
mapDriver.getCounters().findCounter(SurfaceCounters.NONLINEARS).getValue());
Assert.assertEquals("UNKNOWN count incorrect.",
0,
mapDriver.getCounters().findCounter(SurfaceCounters.UNKNOWN).getValue());
PowerMockito.verifyStatic(Mockito.times(1));
PopulatorPreprocessor.processLinearSurface(rs);
// set logging level back to it's original state
so as not to affect other tests
Logger.getRootLogger().setLevel(originalLevel);
}
} |
详解
上面的代码中,我们仅仅检测数据的ID和类型,舍弃非路面数据,进行计数,以及处理路面数据。让我们来看一下第一个测试用例。
testMap_INTERSECTION
这个测试用例的预期结果应该是
SurfaceCounters.NONLINEARS 类型计数器应该自增。
for循环应该可以正常工作,即使没有运行到循环体中的PopulatorPreprocessor.processLinearSurface(surface)方法。
另外两种计数器SurfaceCounters.ROADS和SurfaceCounters.UNKNOWN
不会自增。
这是一个mapper的测试,所以我们先初始化一个mapper的驱动。注意四个类型参数必须与测试目标的类型参数匹配。
private MapDriver<LongWritable, BytesWritable, LongWritable, BytesWritable> mapDriver; @Before public void setUp() { MergeAndSplineMapper mapper = new MergeAndSplineMapper(); mapDriver = new MapDriver<LongWritable, BytesWritable, LongWritable, BytesWritable>(); mapDriver.setMapper(mapper); } |
在定义单元测试用例方法的时候使用IOException
Mapper可能会抛出IOException。在JUnit中,开发人员可以通过catch或throw来处理测试目标代码抛出的异常。注意,这里我们并不是专门测试异常情况,所以,我不建议让测试用例方法去捕捉(catch)测试目标代码的异常,而是让测试目标抛出(throw)它们。如果测试目标发生了异常,测试会失败,而这恰恰是我们想要的结果。如果你并非专门测试异常情况,但是却捕捉了测试目标代码的异常,这往往会造成不必要的麻烦。你大可以抛出这些异常并让测试用例失败。
@Test public void testMap_INTERSECTION() throws IOException { |
然后初始化测试桩。为了测试if-else块,我们要提供路面类型为RoadType.INTERSECTION的数据。
LinkSurfaceMap lsm = new LinkSurfaceMap(); RoadSurface rs = new RoadSurface(Enums.RoadType.INTERSECTION); byte[] lsmBytes = append(lsm, rs); |
我们用PowerMock来模拟调用类型PopulatorPreprocessor的静态方法。PopulatorPreprocessor是一个拥有业务逻辑的独立的类型。在类级别上,我们用
@RunWith来初始化PowerMock。通过 @PrepareForTest,我们告诉PowerMock去模拟哪个有静态方法的类型。PowerMock支持EasyMock和Mockito。这里我们使用Mockito,所以我们使用了相关类型PowerMockito。我们通过调用PowerMockito.mockStatic来模拟调用静态方法。
@RunWith(PowerMockRunner.class) @PrepareForTest(PopulatorPreprocessor.class) PowerMockito.mockStatic(PopulatorPreprocessor.class); |
输入之前创建的测试桩并且运行mapper。
mapDriver.withInput(new LongWritable(1234567), new BytesWritable(lsmBytes)); mapDriver.runTest(); |
最后,查看结果。SurfaceCounters.NONLINEARS 类型的计数器自增了一次,而SurfaceCounters.ROADS
类型的计数器和SurfaceCounters.UNKNOWN类型的计数器没有自增。我们可以用JUnit的assetEquals方法来检测结果。这个方法的第一个参数是一个String类型的可选参数,用来表示断言的错误提示。第二个参数是断言的预期结果,第三个参数是断言的实际结果。assetEquals方法可以输出非常友好的错误提示,它的格式是“expected:
<x> but was: <y>.”。比如说,下面第二个断言没有通过的话,我们就可以得到一个错误语句“java.lang.AssertionError:
NONLINEARS count incorrect. expected:<1> but was:<0>.
“。
Assert.assertEquals("ROADS count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.ROADS).getValue()); Assert.assertEquals("NONLINEARS count incorrect.", 1, mapDriver.getCounters().findCounter(SurfaceCounters.NONLINEARS).getValue()); Assert.assertEquals("UNKNOWN count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.UNKNOWN).getValue()); |
用下面的语句可以检测PopulatorPreprocessor.processLinearSurface(surface)方法没有被调用过。
PowerMockito.verifyStatic(Mockito.never()); PopulatorPreprocessor.processLinearSurface(rs); |
testMap_ROAD
这个测试用例的预期结果应该是
SurfaceCounters. ROADS 类型计数器应该自增。
PopulatorPreprocessor.processLinearSurface(surface)方法被调用了。
另外两种计数器SurfaceCounters. NONLINEARS 和SurfaceCounters.UNKNOWN
不会自增。
测试驱动模块的初始化与第一个用例相似,但有几点不同。
初始化一个路面类型的测试桩。
RoadSurface rs = new RoadSurface(Enums.RoadType.ROAD); |
设置log4j的debug级别。 在测试目标代码中,只有log4j设置成了debug级别,我们才会打印路面数据。为了测试这个功能点,我们先记录当前的logging级别,然后我们把根logger对象的logging级别设置成debug。
Level originalLevel = Logger.getRootLogger().getLevel(); Logger.getRootLogger().setLevel(Level.DEBUG) |
最后,我们把logging级别重新设置成原来的级别,这样就不会影响其他测试了。
Logger.getRootLogger().setLevel(originalLevel); |
我们看一下测试的结果。SurfaceCounters. ROADS 类型的计数器是自增的。另外两个类型的计数器SurfaceCounters.
NONLINEARS和SurfaceCounters.UNKNOWN都不会自增。
Assert.assertEquals("ROADS count incorrect.", 1, mapDriver.getCounters().findCounter(SurfaceCounters.ROADS).getValue()); Assert.assertEquals("NONLINEARS count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.NONLINEARS).getValue()); Assert.assertEquals("UNKNOWN count incorrect.", 0, mapDriver.getCounters().findCounter(SurfaceCounters.UNKNOWN).getValue()); |
使用下面的代码,可以检测出PopulatorPreprocessor.processLinearSurface(surface)被调用了一次。
PowerMockito.verifyStatic(Mockito.times(1)); PopulatorPreprocessor.processLinearSurface(rs); |
测试Reducer
测试reducer和测试mapper的原理是相似的。区别在于我们需要创建一个ReducerDriver,然后把需要测试的reducer赋值给这个ReducerDriver。
private ReduceDriver<LongWritable, BytesWritable, LongWritable, BytesWritable> reduceDriver; @Before public void setUp() { MyReducer reducer = new MyReducer (); reduceDriver = new ReduceDriver <LongWritable, BytesWritable, LongWritable, BytesWritable>(); reduceDriver.setReducer(reducer); } |
配置MAVEN POM
如果使用JUnit 4,那么还要在Maven的POM.xml配置文件中添加下面的配置项。可以在PowerMock的官方网站上找到Mockito相关的版本信息。
<dependency> <groupId>org.apache.mrunit</groupId> <artifactId>mrunit</artifactId> <version>0.8.0-incubating</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.0-rc1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>1.4.12</version> <scope>test</scope> </dependency> |
在Eclipse中运行
这个单元测试可以像其他JUnit测试一样运行。下面是在Eclipse中运行测试的示例。
结论
MRUnit是一种轻量但非常强大的测试驱动开发的工具。它可以帮助开发人员提高代码测试覆盖率。
|