使用Maven开发mapreduce,使用mapreduce 单元Test测试

本文对应于《Hadoop权威指南》第6章Mapreduce应用开发的内容

通常情况下我们进行mapreduce开发要对每个模块进行单元测试,来检查模块的行为是否正确。本文主要介绍在mvn下进行mapreduce模块单元测试。

使用mvn开发mapreduce有两种方法,一个是源码编译(问题少,你必须知道你在干什么,本文采用的方法);另外一个是借助IDE进行编译(集成度高,设置自动生成,写代码可以自动补全,比较爽)。在使用MyEclipse进行mapreduce开发的过程中,出现了hadoop插件导致的问题,因此,在完全解决hadoop插件的问题之前,我选择了如下方法:

  1. 使用IDE进行代码编写。
  2. 上传到CentOS中,进行maven源码编译。
  3. 在这个过程中要尤其注意pom依赖。

使用MRUnit写单元测试 - v1

MRUnit是一个测试库,它便于将已知的输入传递给mapper或者检查reducer的输出是否符合预期。MRUnit与标准测试框架JUnit一起使用,可以在正常的开发环境中运行MapReduce作业测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
maven工程目录结构

ch6-mr-dev/
|--pom.xml
|--src/
|--main/
|--java/v1/
|--Mapper.java
|--test/
|--java/v1/
|--MapperTest.java
|--target/
|--classes/
|--test-classes/

在源文件下的main目录编写mapper模块和reducer模块。将相应的测试模块放在test目录下。注意package的目录等级。

Mapper模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package v1;

import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.io.*;
import java.io.IOException;

public class MaxTempMapper extends Mapper<LongWritable, Text, Text, IntWritable>{

@Override //加上标签表示对方法的重写,系统会自动检查重写方法是否正确
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException{
String line = value.toString();
String year = line.substring(15, 19);
int airTemp = Integer.parseInt(line.substring(87, 92));
context.write(new Text(year), new IntWritable(airTemp));
}
}

MapperTest模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package v1;

import java.io.IOException;

import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mrunit.mapreduce.MapDriver;

import org.junit.*;

public class MaxTempMapperTest{
@Test
public void processesValidRecord() throws IOException, InterruptedException{
Text value = new Text("0043011990999991950051518004+68750+023550FM-12+0382" +
// Year ^^^^
"99999V0203201N00261220001CN9999999N9-00111+99999999999");
// Temperature ^^^^^
new MapDriver<LongWritable, Text, Text, IntWritable>()
.withMapper(new MaxTempMapper())
.withInput(new LongWritable(0), value)
.withOutput(new Text("1950"), new IntWritable(-11))
.runTest();
}
}

这个测试很简单:内置传输一个天气记录作为mapper的输入,然后检查输出是否是读入的年份和气温。
测试的是mapper,可以使用MRUnit的MapDriver.调用runTest()方法。
@Test 表示该方法可以不用通过main函数入口就可以执行得出运行结果,用于标准测试。注意被@Test修饰的方法必须是public。
.withMapper 指向被测试的Mapper,注意该Mapper需要和MapperTest处在同一个package下,这样才能找到Mapper类。
.withInput 指向输入的key和value。这里因为mapper的原始输入kay是文本的偏移值,所以没有意义,这个key可以随意设定。
.withOutput 指向期望的输出。key(1950)和期望的输出值value(-11)。如果mapper没有输出期望值,则MRUnit测试失败。
根据withOutput()被调用的次数,MapDriver能用来检查0,1或多个输出记录。

mvn编译

1. pom.xml的导入:

在pom.xml中可以引入一个通用的pom基础,如下:
其中hadoop-meta中包含了很多包版本的定义等通用信息。同时添加了用于标准单元测试的junit。注意,在创建mvn project的时候默认的junit版本是3.8.2,而这个版本在导入org.junit.*这个包的时候会报错,这个包属于junit 4版本,所以要在pom.xml中修改junit版本。

1
2
3
4
5
6
<parent>
<groupId>com.fredshao.hadoop</groupId>
<artifactId>hadoop-meta</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../hadoop-meta/</relativePath>
</parent>

pom母版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
hadoop-meta/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.hadoop-meta</groupId>
<artifactId>hadoop-meta</artifactId>
<version>1.0</version>
<packaging>pom</packaging>

<name>hadoop-meta-configuration</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<!-- Component versions are defined here -->
<hadoop.version>2.9.2</hadoop.version>
<hbase.version>1.3.5</hbase.version>
<hive.version>0.13.1</hive.version>
<mrunit.version>1.1.0</mrunit.version>
<spark.version>1.1.0</spark.version>
<sqoop.version>1.4.5</sqoop.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

2. pom.xml需要加载的依赖包:

hadoop-client其实包含了hadoop-common依赖包,所以只用添加一个client就可以了。
mrunit要加上classifier表示是hadoop2版本下的。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
</dependency>
<dependency>
<groupId>org.apache.mrunit</groupId>
<artifactId>mrunit</artifactId>
<version>${mrunit.version}</version>
<classifier>hadoop2</classifier>
<scope>test</scope>
</dependency>

3. MyEclipse下mvn编译报错

在MyEclipse下对上述工程进行编译会得到mvn编译报错:

1
2
3
4
Running v1.MaxTempMapperTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.103 sec <<< FAILURE!
processesValidRecord(v1.MaxTempMapperTest) Time elapsed: 0.032 sec <<< ERROR!
java.lang.NoClassDefFoundError: com/ctc/wstx/io/InputBootstrapper

这个错误并不是因为pom依赖出现问题,而是在MyEclipse下的hadoop插件出现了问题,导致依赖包导入不全。提供的解决帮助也把问题指向了hadoop-eclipse-plugin插件。这个遗留问题后期解决。TBC

4. mvn命令行编译

(1) 执行mvn test测试,测试出错:
test_failure_mapper
可以看到当我们把期待输出的温度调整为-16的时候,实际mapper得到的输出仍然为-11,这是测试出错,会报error。

Reducer与ReducerTest模块

注意导入库的版本问题。
如:
org.apache.hadoop.mapred.Reducer 与 org.apache.hadoop.mapreduce.Reducer
org.apache.hadoop.mrunit.ReduceDriver 与 org.apache.hadoop.mrunit.mapreduce.ReduceDriver

不注意检查经常会导致编译出错。
Reducer程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package v1;

import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class MaxTempReducer extends Reducer<Text, IntWritable, Text, IntWritable>{

@Override
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException{

int maxVal = Integer.MIN_VALUE;
for(IntWritable value:values){
maxVal = Math.max(value.get(), maxVal);
}
context.write(key, new IntWritable(maxVal));
}
}

Reducer Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package v1;

import java.io.IOException;
import java.util.Arrays;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mrunit.mapreduce.ReduceDriver;
import org.junit.Test;

public class MaxTempReducerTest {

@Test
public void returnsMaximumIntegerValues() throws IOException, InterruptedException{

new ReduceDriver<Text, IntWritable, Text, IntWritable>()
.withReducer(new MaxTempReducer())
.withInput(new Text("1950"), Arrays.asList(new IntWritable(10),
new IntWritable(5)))
.withOutput(new Text("1950"), new IntWritable(10))
.runTest();
}
}

Tips

我是实用rz工具将代码传入centos进行mvn编译的,rz有一个问题是,如果目标路径里已经有同名的文件了,他是不会完成传输的。所以经常发现已经提交了代码实际却没有更改的问题。

在使用Eclipse进行mvn test时,报了如下几个错误:

  • java.lang.NoClassDefFoundError: com/ctc/wstx/io/InputBootstrapper
  • java.lang.NoClassDefFoundError: Could not initialize class org.apache.hadoop.io.Text

完全不知道这个错误是为什么会出现的。在源码mvn构建就没有任何问题,所以应该是Eclipse配置的问题。如果你知道这两个报错如何解决请联系我guoliang_shao@u.nus.edu。不胜感激