单元测试mock框架——jmockit实战
2016-09-18 16:24
162 查看
JMockit是google code上面的一个java单元测试mock项目,她很方便地让你对单元测试中的final类,静态方法,构造方法进行mock,功能强大。项目地址在:http://jmockit.googlecode.com http://jmockit.org/。详细用法可以在上面找到答案。
JMockit的测试方式可以通过下面2个途径实现
一.根据用例的测试路径,测试代码内部逻辑
对于这种情景,可以使用jmockit的基于行为的mock方式。在这种方式中,目的是测试单元测试及其依赖代码的调用过程,验证代码逻辑是否满足测试路径。 由于被依赖代码可能在自己单测中已测试过,或者难以测试,就需要把这些被依赖代码的逻辑用预定期待的行为替换掉,也就是mock掉,从而把待测是代码隔离开,这也是单元测试的初衷。 这种方式和白盒测试接近。
二.根据测试用例的输入输出数据,测试代码是否功能运行正常。
对于这种情景,可以使用jmockit基于状态的mock方式。目的是从被测代码的使用角度出发,结合数据的输入输出来检验程序运行的这个正确性。使用这个方式,需要把被依赖的代码mock掉,实际上相当于改变了被依赖的代码的逻辑。通常在集成测试中,如果有难以调用的外部接口,就通过这个方式mock掉,模拟外部接口。 这种方式有点像黑盒测试。
下面根据一个简单例子简单介绍JMockit的几个常用测试场景和使用方法。
被测试类:一个猜骰子点数的类。new Guess(int n)时候指定最大猜数次数,并且生成实际点数。在n次猜测内猜中则输出成功,n次猜测失败后通过failHandle()输出错误。结果输出通过GuessDAO来保存。但GuessDAO还没实现。
[java] view
plain copy
/** 在n次机会随机猜骰子点数 ,结果保存到数据库中 */
public class Guess {
private int maxTryTime; // 最大重试次数
private int tryTime = 0; // 当前重试次数
private int number = (int) (Math.random() * 6); // 目标数字
private GuessDAO guessDAO; // 持久化依赖
public Guess(int maxRetryTime) {
this.maxTryTime = maxRetryTime;
}
public void doit() {
while (tryTime++ < maxTryTime && !tryIt()) {
// 到达最大尝试次数仍不成功则调用handle
if (tryTime == maxTryTime) {
failHandle();
}
}
}
public boolean tryIt() { // 最坏情况下调用maxRetryTime次tryIt(),猜中则保存信息
if (number == randomGuess()) {
guessDAO.saveResult(true, number);
return true;
}
return false;
}
public void failHandle() { // 失败处理,猜不中时调用
guessDAO.saveResult(false, number);
}
private int randomGuess(){ // 猜的随机过程
return (int) (Math.random() * 6);
}
public void setGuessDAO(GuessDAO guessDAO) {
this.guessDAO = guessDAO;
}
}
下面通过3个测试用例来说明如何使用jmockit
以下代码基于jmockit1.0 左右版本,新版去废除了一些功能(如@Mocked不能修饰成员)
1. 测试当没有一次猜中时,代码逻辑如何执行。
先上测试代码:
[java] view
plain copy
public class GuessTest {
@Tested // 表明被修饰实例是将会被自动构建和注入的实例
Guess guess = new Guess(3);
@Injectable // 表明被修饰实例将会自动注入到@Tested修饰的实例中,并且会自动mock掉,除非在测试前被赋值
GuessDAO guessDAO;
/**
* 连续3次失败
*/
@Test
public void behaviorTest_fail3time() {
new Expectations() { // Expectations中包含的内部类区块中,体现的是一个录制被测类的逻辑。
@Mocked(methods="tryIt") // 表明被修饰的类对tryIt()方法进行mock。
Guess g;
{
g.tryIt(); // 期待调用Guess.tryIt()方法
result = false; // mock掉返回值为false(表明猜不中)
times = 3; // 期待以上过程重复3次
guessDAO.saveResult(false, anyInt); // 期待调用guessDAO把猜失败的结果保存
}
};
guess.doit(); // 录制完成后,进行实际的代码调用,也称回放(replay)
}
}
说明下这个测试代码的目的: 测试行为是guess.doit(),代码期望在调用doit()函数后,会发生:
1.调用tryIt,并把结果mock为false;
2.重复第一步3次;
3.把结果通过guessDAO保存。即调用3次均猜错数字
可以看出,JMockit在基于行为的测试中,体现3个步骤。第一个是脚本录制,也就是把期望的行为记录下来。在上面例子中,在Expectation内部类的区块中的代码就是期待发生的行为。第二是回放,也就是guess.doit()触发的过程。第三是检验,在这里没有确切体现出,但是的确发生着检验:假设doit方法调用后,代码的逻辑没有符合录制过程中的脚本的行为,那么测试结果失败(其实Jmockit有专门的Verifications做检验,但是这里Expecation已经包含了这个功能,如果用NonStrictExpecation就需要有检验块)。
再介绍下这段代码中用到的各个JMockit元素(结论源自文档及自己代码测试):
@Tested和@Injectable: 对@Tested对象判断是否为null,是则通过合适构造器初始化,并实现依赖注入。调用构造方法时,会尝试使用@Injectable的字段进行构造器注入。普通注入时,@Injectable字段如果没有在测试方法前被赋值,其行为将会被mock成默认值(静态方法和构造函数不会被mock掉)。Injectable最大作用除了注入,还有就是mock的范围只限当前注释实例。一句话:@Injectable的实例会自动注入到@Tested中,如果没初始赋值,那么JMockit将会以相应规则初始化。
@Mocked:@Mocked修饰的实例,将会把实例对应类的所有实例的所有行为都mock掉(无论构造方法,还是private,protected方法,够霸气吧)。在Expectation区块中,声明的成员变量均默认带有@Mocked,但是本例没有省略,是因为@Mocked会mock掉所有方法,而回放的代码中doit函数我们是不希望它也被mock,所以通过method="tryIt"来设置被mock的类只对tryIt方法进行mock。
Expectations:这是录制期望发生行为的地方。result和times都是其内定成员变量。result可以重定义录制行为的返回值甚至通过Delegate来重定义行为,times是期待录制行为的发生次数。在Expectations中发生的调用,均会被mock。由于没定义result,所以guessDAO.saveResult()调用的结果返回空。
2. 当多次失败后,最后一次猜数成功时,代码逻辑如何执行。
在上面的测试代码中,加多一个测试方法:
[java] view
plain copy
/**
* 两次失败,第三次猜数成功
*/
@Test
public void behaviorTest_sucecess() {
new Expectations(Guess.class) { // 构造函数可以传入Class或Instance实例
{
guess.tryIt();
result = false;
times=2;
invoke(guess, "randomGuess", new Object[]{}); // invoke()能调用私有方法
result = (Integer)getField(guess, "number"); // getField()能操作私有成员
guessDAO.saveResult(true, (Integer)getField(guess, "number"));
}
};
guess.doit();
}
第二个测试用例是期待先猜2次失败,第3次猜中。
所以录制中会先调用2次tryIt并返回false,在发生第3次调用时,通过invoke调用私有方法randomGuess,并期待其返回被测实例的私有成员number,通过这种作弊的方式,自然肯定能在第三次猜中数字。最后期待guessDAO把结果保存。
这段代码和之前的区别是,在Expectation中没定义成员变量,而把Guess.class显式地通过构造函数传入。这么做也是为了只对tryIt方法mock,因为在Expectation构造函数传入Class对象或Instance对象后,只会区块内Class或Instance对应的行为进行mock。
通过以上2个基于行为mock的例子,应该对JMockit如何测试代码内部逻辑有了解了吧。下面再对基于状态的mock介绍:
3. 模拟正常猜骰子,观察输出猜中的概率
再加入第三各测试方法:
[java] view
plain copy
/**
* 模拟正常执行,计算抽中概率,把DAO mock掉
*/
@Test
public void stateTest_mockDAO() {
final Map<Integer, Integer> statMap = new HashMap<Integer, Integer>(); // statMap.get(0)为猜中次数,statMap.get(1)为失败次数
statMap.put(0, 0);
statMap.put(1, 0);
guessDAO = new MockUp<GuessDAO>() { // MockUp用来定义新的代码逻辑
@SuppressWarnings("unused")
@Mock
public boolean saveResult(boolean isSuccess, int n) {
if (isSuccess) {
statMap.put(0, statMap.get(0)+1);
System.out.println("you guess it! dice:" + n);
} else {
statMap.put(1, statMap.get(1)+1);
System.out.println("you didn't guess it. dice:" + n);
}
return true;
}
}.getMockInstance();
for (int i=0; i<1000; i++) {
Guess guess = new Guess(3);
guess.setGuessDAO(guessDAO);
guess.doit();
}
System.out.println("hit" + statMap.get(0));
System.out.println("not hit" + statMap.get(1));
double rate =((double) statMap.get(0)) / (statMap.get(0)+statMap.get(1));
System.out.println("hit rate=" + rate);
}
第三个用例目的是,测试在指定尝试次数下猜中数字的概率。这就不再盯着代码内部逻辑,而从整体功能角度进行测试,把内部无法调用的的依赖接口mock掉。
在基于状态的mock中,看不到了Expectations,@Mocked等字样了。取而代之的是MockUp,@Mock。
代码中对GuessDAO的保存方法进行了重定义。让其直接从控制带输出消息。
通过这种方式,不仅可以进行功能黑盒测试,还可以尽快地让测试代码跑起来。
MockUp中的泛型类型是被重定义的类,重定义的方法需要和原类中的方法签名一致。但是,static方法可以省区static关键字。如:
[java] view
plain copy
new MockUp<Calendar>() {
@Mock
public Calendar getInstance() {
return calendar1;
}
};
至此,通过三个例子,把JMockit的2个测试方式简单介绍了。但是JMockit的功能不仅如此,详细能请查看官方文档和实例。
JMockit的测试方式可以通过下面2个途径实现
一.根据用例的测试路径,测试代码内部逻辑
对于这种情景,可以使用jmockit的基于行为的mock方式。在这种方式中,目的是测试单元测试及其依赖代码的调用过程,验证代码逻辑是否满足测试路径。 由于被依赖代码可能在自己单测中已测试过,或者难以测试,就需要把这些被依赖代码的逻辑用预定期待的行为替换掉,也就是mock掉,从而把待测是代码隔离开,这也是单元测试的初衷。 这种方式和白盒测试接近。
二.根据测试用例的输入输出数据,测试代码是否功能运行正常。
对于这种情景,可以使用jmockit基于状态的mock方式。目的是从被测代码的使用角度出发,结合数据的输入输出来检验程序运行的这个正确性。使用这个方式,需要把被依赖的代码mock掉,实际上相当于改变了被依赖的代码的逻辑。通常在集成测试中,如果有难以调用的外部接口,就通过这个方式mock掉,模拟外部接口。 这种方式有点像黑盒测试。
下面根据一个简单例子简单介绍JMockit的几个常用测试场景和使用方法。
被测试类:一个猜骰子点数的类。new Guess(int n)时候指定最大猜数次数,并且生成实际点数。在n次猜测内猜中则输出成功,n次猜测失败后通过failHandle()输出错误。结果输出通过GuessDAO来保存。但GuessDAO还没实现。
[java] view
plain copy
/** 在n次机会随机猜骰子点数 ,结果保存到数据库中 */
public class Guess {
private int maxTryTime; // 最大重试次数
private int tryTime = 0; // 当前重试次数
private int number = (int) (Math.random() * 6); // 目标数字
private GuessDAO guessDAO; // 持久化依赖
public Guess(int maxRetryTime) {
this.maxTryTime = maxRetryTime;
}
public void doit() {
while (tryTime++ < maxTryTime && !tryIt()) {
// 到达最大尝试次数仍不成功则调用handle
if (tryTime == maxTryTime) {
failHandle();
}
}
}
public boolean tryIt() { // 最坏情况下调用maxRetryTime次tryIt(),猜中则保存信息
if (number == randomGuess()) {
guessDAO.saveResult(true, number);
return true;
}
return false;
}
public void failHandle() { // 失败处理,猜不中时调用
guessDAO.saveResult(false, number);
}
private int randomGuess(){ // 猜的随机过程
return (int) (Math.random() * 6);
}
public void setGuessDAO(GuessDAO guessDAO) {
this.guessDAO = guessDAO;
}
}
下面通过3个测试用例来说明如何使用jmockit
以下代码基于jmockit1.0 左右版本,新版去废除了一些功能(如@Mocked不能修饰成员)
1. 测试当没有一次猜中时,代码逻辑如何执行。
先上测试代码:
[java] view
plain copy
public class GuessTest {
@Tested // 表明被修饰实例是将会被自动构建和注入的实例
Guess guess = new Guess(3);
@Injectable // 表明被修饰实例将会自动注入到@Tested修饰的实例中,并且会自动mock掉,除非在测试前被赋值
GuessDAO guessDAO;
/**
* 连续3次失败
*/
@Test
public void behaviorTest_fail3time() {
new Expectations() { // Expectations中包含的内部类区块中,体现的是一个录制被测类的逻辑。
@Mocked(methods="tryIt") // 表明被修饰的类对tryIt()方法进行mock。
Guess g;
{
g.tryIt(); // 期待调用Guess.tryIt()方法
result = false; // mock掉返回值为false(表明猜不中)
times = 3; // 期待以上过程重复3次
guessDAO.saveResult(false, anyInt); // 期待调用guessDAO把猜失败的结果保存
}
};
guess.doit(); // 录制完成后,进行实际的代码调用,也称回放(replay)
}
}
说明下这个测试代码的目的: 测试行为是guess.doit(),代码期望在调用doit()函数后,会发生:
1.调用tryIt,并把结果mock为false;
2.重复第一步3次;
3.把结果通过guessDAO保存。即调用3次均猜错数字
可以看出,JMockit在基于行为的测试中,体现3个步骤。第一个是脚本录制,也就是把期望的行为记录下来。在上面例子中,在Expectation内部类的区块中的代码就是期待发生的行为。第二是回放,也就是guess.doit()触发的过程。第三是检验,在这里没有确切体现出,但是的确发生着检验:假设doit方法调用后,代码的逻辑没有符合录制过程中的脚本的行为,那么测试结果失败(其实Jmockit有专门的Verifications做检验,但是这里Expecation已经包含了这个功能,如果用NonStrictExpecation就需要有检验块)。
再介绍下这段代码中用到的各个JMockit元素(结论源自文档及自己代码测试):
@Tested和@Injectable: 对@Tested对象判断是否为null,是则通过合适构造器初始化,并实现依赖注入。调用构造方法时,会尝试使用@Injectable的字段进行构造器注入。普通注入时,@Injectable字段如果没有在测试方法前被赋值,其行为将会被mock成默认值(静态方法和构造函数不会被mock掉)。Injectable最大作用除了注入,还有就是mock的范围只限当前注释实例。一句话:@Injectable的实例会自动注入到@Tested中,如果没初始赋值,那么JMockit将会以相应规则初始化。
@Mocked:@Mocked修饰的实例,将会把实例对应类的所有实例的所有行为都mock掉(无论构造方法,还是private,protected方法,够霸气吧)。在Expectation区块中,声明的成员变量均默认带有@Mocked,但是本例没有省略,是因为@Mocked会mock掉所有方法,而回放的代码中doit函数我们是不希望它也被mock,所以通过method="tryIt"来设置被mock的类只对tryIt方法进行mock。
Expectations:这是录制期望发生行为的地方。result和times都是其内定成员变量。result可以重定义录制行为的返回值甚至通过Delegate来重定义行为,times是期待录制行为的发生次数。在Expectations中发生的调用,均会被mock。由于没定义result,所以guessDAO.saveResult()调用的结果返回空。
2. 当多次失败后,最后一次猜数成功时,代码逻辑如何执行。
在上面的测试代码中,加多一个测试方法:
[java] view
plain copy
/**
* 两次失败,第三次猜数成功
*/
@Test
public void behaviorTest_sucecess() {
new Expectations(Guess.class) { // 构造函数可以传入Class或Instance实例
{
guess.tryIt();
result = false;
times=2;
invoke(guess, "randomGuess", new Object[]{}); // invoke()能调用私有方法
result = (Integer)getField(guess, "number"); // getField()能操作私有成员
guessDAO.saveResult(true, (Integer)getField(guess, "number"));
}
};
guess.doit();
}
第二个测试用例是期待先猜2次失败,第3次猜中。
所以录制中会先调用2次tryIt并返回false,在发生第3次调用时,通过invoke调用私有方法randomGuess,并期待其返回被测实例的私有成员number,通过这种作弊的方式,自然肯定能在第三次猜中数字。最后期待guessDAO把结果保存。
这段代码和之前的区别是,在Expectation中没定义成员变量,而把Guess.class显式地通过构造函数传入。这么做也是为了只对tryIt方法mock,因为在Expectation构造函数传入Class对象或Instance对象后,只会区块内Class或Instance对应的行为进行mock。
通过以上2个基于行为mock的例子,应该对JMockit如何测试代码内部逻辑有了解了吧。下面再对基于状态的mock介绍:
3. 模拟正常猜骰子,观察输出猜中的概率
再加入第三各测试方法:
[java] view
plain copy
/**
* 模拟正常执行,计算抽中概率,把DAO mock掉
*/
@Test
public void stateTest_mockDAO() {
final Map<Integer, Integer> statMap = new HashMap<Integer, Integer>(); // statMap.get(0)为猜中次数,statMap.get(1)为失败次数
statMap.put(0, 0);
statMap.put(1, 0);
guessDAO = new MockUp<GuessDAO>() { // MockUp用来定义新的代码逻辑
@SuppressWarnings("unused")
@Mock
public boolean saveResult(boolean isSuccess, int n) {
if (isSuccess) {
statMap.put(0, statMap.get(0)+1);
System.out.println("you guess it! dice:" + n);
} else {
statMap.put(1, statMap.get(1)+1);
System.out.println("you didn't guess it. dice:" + n);
}
return true;
}
}.getMockInstance();
for (int i=0; i<1000; i++) {
Guess guess = new Guess(3);
guess.setGuessDAO(guessDAO);
guess.doit();
}
System.out.println("hit" + statMap.get(0));
System.out.println("not hit" + statMap.get(1));
double rate =((double) statMap.get(0)) / (statMap.get(0)+statMap.get(1));
System.out.println("hit rate=" + rate);
}
第三个用例目的是,测试在指定尝试次数下猜中数字的概率。这就不再盯着代码内部逻辑,而从整体功能角度进行测试,把内部无法调用的的依赖接口mock掉。
在基于状态的mock中,看不到了Expectations,@Mocked等字样了。取而代之的是MockUp,@Mock。
代码中对GuessDAO的保存方法进行了重定义。让其直接从控制带输出消息。
通过这种方式,不仅可以进行功能黑盒测试,还可以尽快地让测试代码跑起来。
MockUp中的泛型类型是被重定义的类,重定义的方法需要和原类中的方法签名一致。但是,static方法可以省区static关键字。如:
[java] view
plain copy
new MockUp<Calendar>() {
@Mock
public Calendar getInstance() {
return calendar1;
}
};
至此,通过三个例子,把JMockit的2个测试方式简单介绍了。但是JMockit的功能不仅如此,详细能请查看官方文档和实例。
相关文章推荐
- 单元测试mock框架——jmockit实战
- 单元测试mock框架——jmockit实战
- shiro实战系列(十三)之单元测试
- 单元测试mock框架——jmockit实战
- NHibernate+WCF项目实战(三)使用WCF对外提供Webservices接口并进行单元测试
- 使用JMockit编写java单元测试
- Django实战(10):单元测试
- 实战单元测试
- javaWeb实战教程4-jdbc连接数据库和junit单元测试
- NHibernate+WCF项目实战(三)使用WCF对外提供Webservices接口并进行单元测试
- 单元测试系列:Mock工具Jmockit使用介绍
- 走进单元测试三:实战单元测试
- spring boot学习系列:spring boot的单元测试实战
- 基于Spring MVC做单元测试 —— 使用JMockit
- Vc2013实战(3) 创建原生C++单元测试项目
- 单元测试中mock的使用及mock神器jmockit实践
- Mocha的单元测试实战
- 单元测试(H2等)和持续集成(Hudson)实战简介
- C# WPF MVVM 实战 – 2.4 单元测试
- Apworks框架实战(三):单元测试与持续集成