天道不一定酬所有勤
但是,天道只酬勤

[转]单元测试mock框架——jmockit实战

JMockit是google code上面的一个java单元测试mock项目,她很方便地让你对单元测试中的final类,静态方法,构造方法进行mock,功能强大。项目地址在: http://jmockit.org/。详细用法可以在上面找到答案。 JMockit的测试方式可以通过下面2个途径实现

一.根据用例的测试路径,测试代码内部逻辑

对于这种情景,可以使用jmockit的基于行为的mock方式。在这种方式中,目的是测试单元测试及其依赖代码的调用过程,验证代码逻辑是否满足测试路径。 由于被依赖代码可能在自己单测中已测试过,或者难以测试,就需要把这些被依赖代码的逻辑用预定期待的行为替换掉,也就是mock掉,从而把待测是代码隔离开,这也是单元测试的初衷。 这种方式和白盒测试接近。

二.根据测试用例的输入输出数据,测试代码是否功能运行正常。

对于这种情景,可以使用jmockit基于状态的mock方式。目的是从被测代码的使用角度出发,结合数据的输入输出来检验程序运行的这个正确性。使用这个方式,需要把被依赖的代码mock掉,实际上相当于改变了被依赖的代码的逻辑。通常在集成测试中,如果有难以调用的外部接口,就通过这个方式mock掉,模拟外部接口。 这种方式有点像黑盒测试。

下面根据一个简单例子简单介绍JMockit的几个常用测试场景和使用方法。

被测试类:一个猜骰子点数的类。new Guess(int n)时候指定最大猜数次数,并且生成实际点数。在n次猜测内猜中则输出成功,n次猜测失败后通过failHandle()输出错误。结果输出通过GuessDAO来保存。但GuessDAO还没实现。

/** 在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 1. 测试当没有一次猜中时,代码逻辑如何执行。

先上测试代码: 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. 当多次失败后,最后一次猜数成功时,代码逻辑如何执行。

在上面的测试代码中,加多一个测试方法:

/** 
  * 两次失败,第三次猜数成功 
  */  
 @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. 模拟正常猜骰子,观察输出猜中的概率

再加入第三各测试方法:

/** 
 * 模拟正常执行,计算抽中概率,把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关键字。如:

new MockUp<Calendar>() {  
    @Mock  
    public Calendar getInstance() {  
        return calendar1;  
    }  
};  

至此,通过三个例子,把JMockit的2个测试方式简单介绍了。但是JMockit的功能不仅如此,详细能请查看官方文档和实例。

=============

过程中遇到还未解决的疑问:

  1. 基于行为的mock,需要对回放的类的具体类型类mock,没法针对父类类型mock?

    Guess g = new Guess(3);
    new Expectations() {
    @Mocked(methods=”tryIt”)
    GuessParent mg; // 对Guess父类进行mock
    {
    mg.tryIt();result=true;
    }
    };
    g.doit();

假如声明的mg类型是Guess的父类,则回放中调用Guess.doit()将不能捕捉道mg.tryIt();导致测试失败。 除非在expectation构造函数传入实例g才可以。

  1. 基于行为的动态mock, 文档说: If the Class object for a given class is passed, the methods and constructors defined in that class are considered for mocking,but not the methods and constructors of its super-classes. If an instance of a given class is passed, then all methods defined in the whole class hierarchy, from the concrete class of the given instance up to (but not including) Object, are considered for mocking; the constructors of these classes, however, are not (since an instance was already created). 粗体的不是很理解,是说mock的父类的方法和构造函数不被mock?但测试结果却不是这样

原文地址:http://blog.csdn.net/ultrani/article/details/8993364

(全文完)
欢迎关注HollisChuang微信公众账号
打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。HollisChuang's Blog » [转]单元测试mock框架——jmockit实战

分享到:更多 ()

HollisChuang's Blog

联系我关于我