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