在MapReduce中,map函数和reduce函数的独立测试非常方便,这是由函数风格决定的。MRUnit(http://mrunit.apache.org/)是一个测试库,它便于将已知的输人传递给mapper或者检查reducer的输出是否符合预期。MRUnit与标准的测试执行框架(如JUnit)—起使用,因此可以在正常的开发环境中运行MapReduce作业的测试。
1.关于Mapper
范例是一个mapper的测试。MaxTemperatureMapper的单元测试import java.io.IOException; import org.apache.hadoop.io.*; import org.apache.hadoop.mrunit.mapreduce.MapDriver; import org.junit.*; public class MaxTemperatureMapperTest{ @Test public void processesVaIidRecord() throws IOException,InterruptedException{ Text value=new Text("....."); new MapDriver<LongWritable,Text,Text,IntWritable>() .withMapper(new MaxTemperatureMapper()) .withlnput(new LongWritable(0),value) .withOutput(new Text("1959"),new IntWritabIe(-11)) .runTest(); } }测试很简单:传递一个天气记录作为mapper的输人,然后检查输出是否是读人的年份和气温。由于测试的是mapper,所以可以使用MapDriver。在调用runTest()方法执行这个测试之前,在test(MaxTemperatureMapper)下配置mapper、输人key和
值、期望的输出key(1959,表示年份的Text对象)和期望的输出值(-11℃,表示温度的lntwritable)。如果mapper没有输出期望的值,MRUnit测试失败。注意,由于mapper忽略输人key,因此,输人key可以设置为任何值。在测试驱动的方式下,范例创建了一个能够通过测试的Mapper实现。由于本章要进行类的扩展,所以每个类被放在包含版本信息的不同包中。例如,v1.MaxTemperatureMapper是MaxTemperatureMapper的第一个版本。当然,不重新打包实际上也可以对类进行扩展。
范例,通过了MaxTemperatureMapper测试的第一个版本Mapper函数
public class MaxTemperatureMapper extends Mapper<LongWritable,Text,IntWritab1e>{ @Override public void map(LongWritable key,Text value,Context context)throws IOException,InterruptedException{ String line=value.toString(); String year=line.substring(15,19); int airTemperature=Integer.parseInt(line.substring(87,92)); context.write(new Text(year),new IntWritable(airTemperature)); } }
这是一个非常简单的实现,从行中抽出年份和气温,并将它们写到context中。现在,让我们增加一个缺失值的测试,该值在原始数据中表示气温+9999:
@Test public void ignoresMissingTemperatureRecord()throws IOException,InterruptedException{ Textvalue=newText("30119999919551518-+68750+9235FM.12+9382"+ //Year^ "99999v9293201N09261229991(N9999999N9+99991+99999999999"); //Temperature new MapDriver<LongWritab1e,Text,Text,IntWritable>() .withMapper(new MaxTemperatureMapper()) .withlnput(new LongWritabIe(0),value) ·runTest(); }
根据with0utput()被调用的次数,MapDriver能用来检查0、1或多个输出记录。在这个测试中由于缺失温度的记录已经被过滤掉,该测试保证对于这种特定的输人值不产生任何输出。
由于没有考虑到+9999这样一种特殊情况,新测试以失败告终。与其在mapper中加人更多的逻辑考虑,还不如给出一个解析类来封装解析逻辑更有意义。详情可以参见下面范例。
范例,该类解析NCDC格式的气温记录
public class NcdcRecordParser{ private static final int MISSING_TEMPERATURE=9999; private String year; private int airTemperature; private String quality; public void parse(String record){ year=record.substring(15,19); String airTemperatureString; //RemoveLeadingplussignasparserntdoesn'tethemre刁0v0乃 if(record.charAt(87)=='+'){ airTemperatureString=record.substring(88,92); }else{ airTemperatureString=record.substring(87,92); } airTemperature=lnteger.parselnt(airTemperatureString); quality=record.substring(92,93); } public void parse(Text record){ parse(record·toString(); } public boolean isValidTemperature(){ return airTemperature!=MISSING_TEMPERATURE&&quality.matches("[04159]"); } public String getYear(){ return year; } public int getAirTemperature(){ return airTemperature; } }
最终的mapper(第二个版本)相当简单。它只需要调用解析类的parse()方法,就能对输人行中感兴肉字段进行解析,用isValidTemperature()查询方法可以检查是否是合法气温,如果是,则使用解析类的获取方法得到年份和值。需要注意是,我们在isValidTemperature()中检查是否有遗漏气温值的同时,也对质量状态字段进行检查,以便过滤掉不准确的气温读取值。
创建解析类的另一个好处是:相似作业的mapper不需要重写代码。并且,对于更多的有针对性的测试而言,该方法也提供了一个机会,即可以直接针对解析类编写单元测试。
范例,这个mapper使用utility类来解析记录
public class MaxTemperatureMapper extends Mapper<LongWritable,Text,Text,IntWritable>{ private NcdcRecordParser parser=newNcdcRecordParser(); @Override public void map(LongWritable key,Text value,Context context)throws IOException,InterruptedException{ parser.parse(value); if(parser.isValidTemperature()){ context.write(newText(parser.getYear()),new IntWritabIe(parser.getAirTemperature())); } } }
这个对mapper的测试通过后,我们接下来写reducer。
2.关于Reducer
reducer必须找出指定键的最大值。这是针对此特性的一个简单的测试,其中使用了一个ReduceDriver。@Test public void returnsMaximumIntegerInVaIues() throws IOException,InterruptedException{ newReduceDriver<Text,IntWritable,Text,IntWritabIe>() .withReducer(new MaxTemperatureReducer()) .withInputKey(newText("1950"),Arrays.asList(newIntWritable(10),new IntWritable(5))) .withOutput(newText("1950"),newIntWritab1e(1e)) ·runTest(); }
我们对一些IntWritable值构建一个迭代器来验证MaxTemperatureReducer能找到最大值。范例里的代码是一个通过测试的MaxTemperatureReducer的实现。
范例,用来计算最高气温的reducer
public class MaxTemperatureReducer extends Reducer<Text,IntWritable,Text,IntWritable>{ @Override public void reduce(Text key,IterabIe<IntWritabIe>values,Context context)throws IOException,InterruptedException{ int maxValue=Integer.MIN_VALUE; for(IntWritabIe value:values){ maxValue=Math.max(maxVaIue,value.get()); context.write(key,new IntWritabIe(maxVaIue)); } } }