map/reduce unit test

由于Map/Reudce Job是运行在hadoop分布式环境中,所以给日常开发map/reduce的时候带来了很多不便,包括调试或者测试等。但是Apache下面一个开源的项目(MRUnit)可以对Map/Reduce进行单元测试,这样就可以使用单元测试用例来对Map/Reduce进行Debug,从而也可能通过丰富的测试用例来进行测试。可以在本地开发机上保证基本业务正确的前提下,再发布到hadoop分布式环境中解决一些分布式带来的问题。

MRUnit的具体使用官网中已经具体的使用说明。(MRUnit web site)

下面主要介绍一下Map/Reudce和MRUnit的最基本的原理,及对MRUnit中进行一些单独的展现来实现一些复杂的业务。

1. MAP

Map的基本原理(为了说明MRUnit在Map的基本原理),Map主要是读取原数据进行map操作,Hadoop Map/Reduce框架在map阶段调用org.apache.hadoop.mapreduce.Mapper.run方法,具体如下:

  public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    while (context.nextKeyValue()) {
      map(context.getCurrentKey(), context.getCurrentValue(), context);
    }
    cleanup(context);
  }

从上面的方法,可以看到在run中在开始和结果分别做了setup和cleanup操作,核心操作就是通过map context读取数据,并用读取到的key/value数据调用具体的map逻辑(通常map的逻辑是我们来实现),最终通过context.write把map的结果输出到中间结果(通常以文件形式)中提供reduce使用。

了解了map的基本原因之后,其实MRUnit主要做的就是mock一个map context,用于接受我们在单元测试中模拟数据,及对map的结果与测试用例中期望的结果进行比较。MapDriver通过 withInput和 withOutput来接受input和期望output,调用runTest来执行具体的map测试用例,如下:

        mapDriver.withInput(new ChunkKey(), chunkWritable);
        mapDriver.withOutput(new BytesWritable(SerializeUtil.serializeToBytes(multipleObject)), new BytesWritable(SerializeUtil.serializeToBytes(chunk)));
        mapDriver.runTest();
runTest的具体实现如下,执行具体的run得到map的output,再与期望的output比较:

public void runTest(final boolean orderMatters) {
    LOG.debug("Mapping input (" + inputKey + ", " + inputVal + ")");
    try {
      final List<Pair<K2, V2>> outputs = run();
      validate(outputs, orderMatters);
      validate(counterWrapper);
    } catch (final IOException ioe) {
      LOG.error("IOException in mapper", ioe);
      throw new RuntimeException("IOException in mapper: ", ioe);
    }
  }
run的具体实现如下,通过input来mock一个map context,然后调用map的run方法返回map的output。

    final List<Pair<K1, V1>> inputs = new ArrayList<Pair<K1, V1>>();
    inputs.add(new Pair<K1, V1>(inputKey, inputVal));
    final InputSplit inputSplit = new MockInputSplit();
    try {
      final MockMapContextWrapper<K1, V1, K2, V2> wrapper = new MockMapContextWrapper<K1, V1, K2, V2>(
          inputs, getCounters(), getConfiguration(), inputSplit);
      final Mapper<K1, V1, K2, V2>.Context context = wrapper.getMockContext();
      myMapper.run(context);
      return wrapper.getOutputs();
    } catch (final InterruptedException ie) {
      throw new IOException(ie);
    }
从上面的代码中可以看到,最主要是需要mock map context,MRUnit中主要mock了map context的nextKeyValue(), getCurrentKey()和getCurrentValue(),mock使用的原数据就是通过withInput输入的值,及write用于把输出写到内部的List对象中,使用 mockito来mock具体的对象,具体如下:

    when(context.nextKeyValue()).thenAnswer(new Answer<Boolean>() {
      @Override
      public Boolean answer(final InvocationOnMock invocation) {
        if (inputs.size() > 0) {
          currentKeyValue = inputs.remove(0);
          return true;
        } else {
          currentKeyValue = null;
          return false;
        }
      }
    });
    when(context.getCurrentKey()).thenAnswer(new Answer<KEYIN>() {
      @Override
      public KEYIN answer(final InvocationOnMock invocation) {
        return currentKeyValue.getFirst();
      }
    });
    when(context.getCurrentValue()).thenAnswer(new Answer<VALUEIN>() {
      @Override
      public VALUEIN answer(final InvocationOnMock invocation) {
        return currentKeyValue.getSecond();
      }
    });
    doAnswer(new Answer<Object>() {
      @Override
      public Object answer(final InvocationOnMock invocation) {
        final Object[] args = invocation.getArguments();
        outputs.add(new Pair(copy(args[0], conf), copy(args[1], conf)));
        return null;
      }
    }).when(context).write((KEYOUT) any(), (VALUEOUT) any());

1.1 Map阶段对MRUnit进行扩展

由于MRUnit只mock了map context中最基本的一些方法,如果使用到别的方法时,需要扩展mock map context,从run方法中可以看到mock出来的context是一个方法级变量,所以无法通过简单的值入来扩展,可以通过继承MapDriver并override  run方法来扩展,如果想在setup中通过context来获取map/reduce job name就可以通过如下这种方式来扩展:

            final Mapper<K1, V1, K2, V2>.Context context = wrapper.getMockContext();
            when(context.getJobName()).thenAnswer(new Answer<String>() {
                @Override
                public String answer(final InvocationOnMock invocation) {
                    return jobName;
                }
            });
            myMapper.run(context);
            return wrapper.getOutputs();
2. Reduce
MRUnit对reduce的处理基本跟map差不多,这里就不多介绍,下面主要介绍在Reduce过程中对MultipleOutputs的支持,因为在很多map/reduce job中都会用到MultipleOutputs来在reduce阶段根据不能的业务输入不同的结果,而MRUnit目前还不支持MultipleOutputs,不过网上已经有人实现了对map/reduce v1的支持,可以通过这个地址 下载。但是目前项目中使用的是map/reduce v2,所以需要自己来实现。其实了解了MRUnit的基本原理之后,基本需要做的只是去mock一个MultipleOutputs对象的write方法,用于把结果写到List中,执行完reduce之后比较这个List与期望值。

	    final MockReduceContextWrapper<K1, V1, K2, V2> wrapper = new MockReduceContextWrapper<K1, V1, K2, V2>(
                    inputs, getCounters(), getConfiguration());
            final Reducer<K1, V1, K2, V2>.Context context = wrapper.getMockContext();
            //mock multiple output object
            MockMultipleOutputs mockMultipleOutputs = new MockMultipleOutputs(context);
            //set multiple output
            setMultipleOutputs(myReducer, mockMultipleOutputs.getMultipleOutputs());
            myReducer.run(context);
            //check output path
            Assert.assertEquals(outputPath,mockMultipleOutputs.getOutputPath());
            //get output list from mock multiple output
            return mockMultipleOutputs.getOutputs();
由于Reduce的结构基本固定,因为map/reduce框架通过这种结构来执行,所以在不改变Reduce结构的前提下,通过反相反射来把mock multiple ouput object注入到reduce中,上面setMultipleOutputs的实现如下:

     private void setMultipleOutputs(Reducer<K1, V1, K2, V2> myReducer,
                                    MultipleOutputs multipleOutputs) throws Exception {
        Field field = getFieldByType(myReducer, MultipleOutputs.class);
        if (field != null) {
            field.setAccessible(true);
            field.set(myReducer, multipleOutputs);
        }
    }
    private Field getFieldByType(Reducer<K1, V1, K2, V2> myReducer, Class<?> clazz) {
        Field[] fields = myReducer.getClass().getDeclaredFields();
        Field candidate = null;
        for (Field field : fields) {
            if (field.getType().isAssignableFrom(clazz)) {
                candidate = field;
                break;
            }
        }
        return candidate;
    }
而mock multiple output主要实现write方法如下,把输入写到内容LIst中,并获取output path来验证reduce输出的目录是否正确:

	try {
            doAnswer(new Answer<Object>() {
                @Override
                public Object answer(final InvocationOnMock invocation) {
                    final Object[] args = invocation.getArguments();
                    outputPath = (String) args[3];
                    outputs.add(new Pair(copy(args[1], conf), copy(args[2], conf)));
                    return null;
                }
            }).when(output).write(anyString(), any(), any(), anyString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
具体使用如下:

reduceDriver.withInput(new BytesWritable(SerializeUtil.serializeToBytes(key)), inputList);
        reduceDriver.withMultipleOutput("/test/url/20121216/tmp_/m/url-data", new BytesWritable(SerializeUtil.serializeToBytes(url)), new BytesWritable(SerializeUtil.serializeToBytes(urlAnalyzerResult)));
        reduceDriver.runTest();

总结:其实了解了MRUnit的基本原理之后,就可以对其进行一些简单的扩展来支持一些较复杂的业务。






你可能感兴趣的:(map/reduce unit test)