编写单元测试
在MapReduce中,map和reduce函数的独立测试非常方便,这是由函数风格决定 的。针对已知输入,得到已知的输出。然而,由于输出写到一个 OutputCollector ,而不是通过简单的方法调用进行返回,所以, OutputCollector需要用一个mock进行替换,以便验证输出的正确性。有几个 Java mock对象框架可以用来建立模拟。这里,我们使用Mockito。虽然目前有许 多这样的框架可用,但是Mockito却以其简洁的语法而著称。
这里描述的所有测试可以在IDE中运行。
mapper
例5-4是一个mapper的测试。
例 5-4. MaxTemperatureMapper 的单元测试
import static org.mockito.Mockito.*; import java.io.IOException; import org.apache.hadoop.io.*; import ong.apache.hadoop.mapred.OutputCollector; import org,junit.*; public class MaxTempenatuneMapperTest { @Test public void pnocessesValidRecond() throws IOException { MaxTemperatureMapper mapper = new MaxTempenatuneMappen(); Text value = new Text("00430119909999919S0051518004+687S0+023SS0FM-12+0382" + // Yean ^^^^ "99999V020B201N00261220001CN9999999N9-00111+99999999999"); // Temperature ^^^^^ OutputCollectonoutput = mock(OutputCollecton.class); mapper.map(null, value, output, null); verify(output).collect(new Text("1950"),new IntWritable(-11)) } }
测试很简单:传递一个天气记录作为mapper的输入,然后检査输出是否是读入的 年份和气温。mapper忽略输入的键和Reporter,因此,任何输入都可以传递,包 括此处的nu11值。为了创建一个OutputCollector,我们调用Mockito的mock() 方法(一个静态导入)来传递一个我们想要模拟类型的类。然后,我们调用mapper 的map()方法来执行测试代码。最后,我们用Mockito的verify()来验证mock对 象已调用了正确的方法和参数。这里,我们在表示年份(1950)的Text对象和表示气 温(-1.1°C)的IntWritable对象调用的例子上验证了 OutputCollector的collect()方法。
在测试驱动的方式下,例5-5创建了一个能够通过测试的的Mapper实现。由于本章 要进行类的扩展,所以每个类被放在包含版本信息的不同包中。例如, vl.MaxTemperatureMapper 是 MaxTemperatureMapper 的第一个版本。当然, 不重新打包实际上也可以对类进行扩展。
例5-5.第一个版本的Mapper函数通过了 MaxTemperatureMapper测试
public class MaxTempenatuneMapper extends MapReduceBase implements Mapper<LongWnitable, Text, Text, IntWnitable> { public void map(LongWnitable key, Text value, OutputCollecton<Text, IntWnitable> output, Reporter reporter) throws IOException { String line = value.toString(); String yean = line.substning(15, 19); int ainTempenatune = Integer.parseInt(line.substning(87, 92)); output.collect(new Text(year), new IntWritable(airTemperature)); } }
这是一个非常简单的实现,从行中抽出年份和气温,在OutputCollector中输 出。现在,让我们增加一个缺失值的测试,该值在原始数据中表示气温+9999:
@Test public void ignonesMissingTempenatuneRecond() throws IOException { MaxTemperatureMapper mapper = new MaxTemperatureMapper(); Text value = new TGXt("0043011990999991950051518004+68750+023550FM-12+0382" + // Yean ^^^^ "99999V0203201N00261220001CN9999999N9+99991+99999999999"); // Temperature ^^^^^ OutputCollecton<Text, IntWnitable>output = mock(OutputCollecton.class); mappen.map(null,value, outputj null); verify(output, never()).collect(any(Text.class), any(IntWritable.class)); }
由于缺失的气温已经被过滤掉,所以在这个测试中,Mockito用来验证 OutputCollector的collect()方法没有被任何Text键或IntWritable值调 用。
由于parselnt()不能解析带加号的整数,所以测试最后抛出NumberFormatException 异常,以失败告终。下面修改此实现(版本2)来处理缺失值:
public void map(LongWritable key, Text value, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException { String line = value.toString(); String year = line.substring(15, 19); String temp = line.substring(87, 92); if (!missing(temp)) { int airTemperature = Integer.parseInt(temp); output.collect(new Text(year), new IntWritable(airTemperature)); } } private boolean missing(String temp) { return temp.equals("+9999"); }
这个测试通过后,我们接下来写reducer。
reducer
reducer必须找出指定键的最大值。这是针对此特性的一个简单的测试。
@Test public void returnsMaximumIntegerInValues() throws IOException { MaxTemperatureReducer reducer = new MaxTemperatureReducer(); Text key = new Text("1950"); Iteratorvalues = Arrays.asList( new IntWritable(10), new IntWritable(5)).iterator(); OutputCollectoroutput = mock(OutputCollector.class); reducer.reduce(key, values, output, null); verify(output).collect(key, new IntWritable(10)); }
我们对一些IntWritable值构建一个迭代器来验证MaxTemperatureReducer能找到 最大值。例5-6里的代码是一个通过测试的MaxTemperatureReducer的实现。注意,我们没有测试空值迭代的情况,可以说根本不需要测试,因为mapper生成的每个 键都有一个值,MapReduce从来不会出现调用空值reducer的情况。
例5-6.用来计算最高气温的reducer
public class MaxTemperatureReducer extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable> { public void reduce(Text key, Iterator<IntWritable> values, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException { int maxValue = Integer.MIN_VALUE; while (values.hasNext()) { maxValue = Math.max(maxValue, values.next().get()); } output.collect(key, new IntWritable(maxValue)); } }