求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Hadoop MapReduce作业的单元测试
 

发布于2013-7-29

 

引言

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是一种轻量但非常强大的测试驱动开发的工具。它可以帮助开发人员提高代码测试覆盖率。

相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践
 
分享到
 
 
     


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...