mock 与 JMockit

mock

碰撞测试是汽车开发活动中的重要组成部分。所有汽车在上市之前都要经过碰撞测试,并公布测试结果。碰撞测试的目的用于评定运输包装件在运输过程中承受多次重复性机械碰撞的耐冲击强度及包装对内装物的保护能力。说简单点就是为了测试汽车在碰撞的时候锁所产生的自身损伤、对车内人员及车外人员、物品等的损伤情况。

进行汽车的碰撞测试时,当然不能让真人来进行测试,一般采用假人来测试。但是为了保证测试的真实性及可靠性,假人的生物力学性能应该和人体一样——比如身体各部分的大小和质量,以及关节的刚性等等,只有这样使用它们的模拟才能和现实相匹配。为了保证覆盖到的情况够全面,一般都会使用各种不同的假人,不同的假人模拟男性或者女性的身体,以及不同身高和年龄的人体。

想想软件测试,其实和汽车的碰撞测试流程差不多。一个软件在发布上线之前都要经过各种测试,并产出测试报告,更严格的一点的要保证单测覆盖率不能低于某个值。和汽车碰撞测试类似,我们在软件测试中也会用到很多“假人”。用这些“假人”的目的也是为了保证测试有效的进行。

why

不知道你在日常开发中有没有遇到过以下问题或需求:

  • 和别人一起做同一个项目,相互之间已经约定好接口。然后你开始开发,开发完自己的代码后,你想测试下你的服务实现逻辑是否正确。但是因为你依赖的只是接口,真正的服务还有开发出来。
  • 还是和上面类似的场景,你要依赖的服务是通过RPC的方式调用的,而外部服务的稳定性很难保证。
  • 对于一个接口或者方法,你希望测试其各种不同情况,但是依赖的服务的执行策略及返回值你没办法决定。
  • 你依赖的服务或者对象很难创建!(比如具体的Web容器)
  • 依赖的对象的某些行为很难触发!(比如网络异常)
  • 以上问题你都没有,但是你要用的那个服务他处理速度实在是太慢了。

上面这些情况都是日常开发测试过程中可能遇到的比较麻烦的问题。这些问题都会大大的提高测试成本。可以说,很多开发人员不愿意写单元测试很大程度上都和以上这六点有关系。幸运的是,mock对象可以解决以上问题。使用mock对象进行的测试就是mock测试。

what

  • mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
  • mock对象,就是非真实对象,是模拟出来的一个对象。可以理解为汽车碰撞测试的那个假人。mock对象就是真实对象在调试期间的代替品。
  • 你创建这样一个“假人”的成本比较低,这个“假人”可以按照你设定的“剧情”来运行。
  • 在Java的单元测试中,很多Mock框架可以使用,用的比较多的有easymock、mockito、powermock、jmockit等。
  • 面向对象开发中,我们通常定义一个接口,使用一个接口来描述这个对象。在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是mock对象。
  • 好了,这篇文章的内容差不多就这些了,主要是让大家知道,在Java中可以使用mock对象来模拟真实对象来进行单元测试,好处很多。下一篇会详细介绍如何使用mockito框架进行单元测试。

JMockit

JMockit是基于JavaSE5中的java.lang.instrument包开发,内部使用ASM库来动态修改Java的字节码,使得Java这种静态语言可以想动态脚本语言一样动态设置被Mock对象私有属性,模拟静态、私有方法行为等等,对于手机开发,嵌入式开发等要求代码尽量简洁的情况下,或者对于被测试代码不想做任何修改的前提下,使用JMockit可以轻松搞定很多测试场景。

20140104100723093

通过如下方式在maven中添加JMockit的相关依赖:

        <dependency>  
            <groupId>com.googlecode.jmockit</groupId>  
            <artifactId>jmockit</artifactId>  
            <version>1.5</version>  
            <scope>test</scope>  
        </dependency>  
        <dependency>  
            <groupId>com.googlecode.jmockit</groupId>  
            <artifactId>jmockit-coverage</artifactId>  
            <version>0.999.24</version>  
            <scope>test</scope>  
        </dependency>复制ErrorOK!

Jmockit有两种Mock方式:基于行为的Mock方式和基于状态的Mock方式:引用单元测试中mock的使用及mock神器jmockit实践中JMockit API和工具如下:

20140104102342843

(1).基于行为的Mock方式:

非常类似与EasyMock和PowerMock的工作原理,基本步骤为:

  • 录制方法预期行为。
  • 真实调用。
  • 验证录制的行为被调用。

通过一个简单的例子来介绍JMockit的基本流程:

  • 要Mock测试的方法如下:
    public class MyObject {
        public String hello(String name){
            return "Hello " + name;
        }
    }
    
  • 使用JMockit编写的单元测试如下:
    @Mocked  //用@Mocked标注的对象,不需要赋值,jmockit自动mock  
    MyObject obj;  
    @Test  
    public void testHello() {  
        new NonStrictExpectations() {//录制预期模拟行为  
            {  
                obj.hello("Zhangsan");  
                returns("Hello Zhangsan");  
                //也可以使用:result = "Hello Zhangsan";  
            }  
        };  
        assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//调用测试方法  
        new Verifications() {//验证预期Mock行为被调用  
            {  
                obj.hello("Hello Zhangsan");  
                times = 1;  
            }  
        };  
    }
    
  • JMockit也可以分类为非局部模拟与局部模拟,区分在于Expectations块是否有参数,有参数的是局部模拟,反之是非局部模拟。
  • 而Expectations块一般由Expectations类和NonStrictExpectations类定义,类似于EasyMock和PowerMock中的Strict Mock和一般性Mock。
  • 用Expectations类定义的,则mock对象在运行时只能按照 Expectations块中定义的顺序依次调用方法,不能多调用也不能少调用,所以可以省略掉Verifications块;
  • 而用NonStrictExpectations类定义的,则没有这些限制,所以如果需要验证,则要添加Verifications块。
  • 上述的例子使用了非局部模拟,下面我们使用局部模拟来改写上面的测试,代码如下:
    @Test  
    public void testHello() {  
        final MyObject obj = new MyObject();  
        new NonStrictExpectations(obj) {//录制预期模拟行为  
            {  
                obj.hello("Zhangsan");  
                returns("Hello Zhangsan");  
                //也可以使用:result = "Hello Zhangsan";  
            }  
        };  
        assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//调用测试方法  
        new Verifications() {//验证预期Mock行为被调用  
            {  
                obj.hello("Hello Zhangsan");  
                times = 1;  
            }  
        };  
    }
    
  • 模拟静态方法:
    @Test  
    public void testMockStaticMethod() {  
        new NonStrictExpectations(ClassMocked.class) {  
            {  
                ClassMocked.getDouble(1);//也可以使用参数匹配:ClassMocked.getDouble(anyDouble);  
                result = 3;  
            }  
        };  
        assertEquals(3, ClassMocked.getDouble(1));  
        new Verifications() {  
            {  
                ClassMocked.getDouble(1);  
                times = 1;  
            }  
        };  
    }
    
  • 模拟私有方法:
  • 如果ClassMocked类中的getTripleString(int)方法指定调用一个私有的multiply3(int)的方法,我们可以使用如下方式来Mock:
    @Test  
    public void testMockPrivateMethod() throws Exception {  
        final ClassMocked obj = new ClassMocked();  
        new NonStrictExpectations(obj) {  
            {  
                this.invoke(obj, "multiply3", 1);//如果私有方法是静态的,可以使用:this.invoke(null, "multiply3")  
                result = 4;  
            }  
        };  
        String actual = obj.getTripleString(1);  
        assertEquals("4", actual);  
        new Verifications() {  
            {  
                this.invoke(obj, "multiply3", 1);  
                times = 1;  
            }  
        };  
    }
    
  • 设置Mock对象私有属性的值: 我们知道EasyMock和PowerMock的Mock对象是通过JDK/CGLIB动态代理实现的,本质上是类的继承或者接口的实现,但是在Java面向对象编程中,基类对象中的私有属性是无法被子类继承的,所以如果被Mock对象的方法中使用到了其自身的私有属性,并且这些私有属性没有提供对象访问方法,则使用传统的Mock方法是无法进行测试的,JMockit提供了设置Mocked对象私有属性值的方法,代码如下: 被测试代码:
    public class ClassMocked {  
        private String name = "name_init";  
        public String getName() {  
            return name;  
        }  
        private static String className="Class3Mocked_init";  
        public static String getClassName(){  
            return className;  
        }  
    }
    
  • 使用JMockit设置私有属性:
    @Test  
    public void testMockPrivateProperty() throws IOException {  
        final ClassMocked obj = new ClassMocked();  
        new NonStrictExpectations(obj) {  
            {  
                this.setField(obj, "name", "name has bean change!");  
            }  
        };  
        assertEquals("name has bean change!", obj.getName());  
    }
    
  • 使用JMockit设置静态私有属性:
    @Test  
    public void testMockPrivateStaticProperty() throws IOException {  
        new NonStrictExpectations(Class3Mocked.class) {  
            {  
                this.setField(ClassMocked.class, "className", "className has bean change!");  
            }  
        };  
        assertEquals("className has bean change!", ClassMocked.getClassName());  
    }
    

(2).基于状态的Mock方式:

  • JMockit上面的基于行为Mock方式和传统的EasyMock和PowerMock流程基本类似,相当于把被模拟的方法当作黑盒来处理,而JMockit的基于状态的Mock可以直接改写被模拟方法的内部逻辑,更像是真正意义上的白盒测试,下面通过简单例子介绍JMockit基于状态的Mock。 被测试的代码如下:
    public class StateMocked {  
        public static int getDouble(int i){  
            return i*2;  
        }  
        public int getTriple(int i){  
            return i*3;  
        }  
    }
    
  • 改写普通方法内容:
    @Test  
    public void testMockNormalMethodContent() throws IOException {  
        StateMocked obj = new StateMocked();  
        new MockUp<StateMocked>() {//使用MockUp修改被测试方法内部逻辑  
            @Mock  
          public int getTriple(int i) {  
                return i * 30;  
            }  
        };  
        assertEquals(30, obj.getTriple(1));  
        assertEquals(60, obj.getTriple(2));  
        Mockit.tearDownMocks();//注意:在JMockit1.5之后已经没有Mockit这个类,使用MockUp代替,mockUp和tearDown方法在MockUp类中  
    }
    
  • 修改静态方法的内容: 基于状态的JMockit改写静态/final方法内容和测试普通方法没有什么区别,需要注意的是在MockUp中的方法除了不包含static关键字以外,其他都和被Mock的方法签名相同,并且使用@Mock标注,测试代码如下:
    @Test  
        public void testGetTriple() {  
            new MockUp<StateMocked>() {  
                @Mock  
                public int getDouble(int i){  
                    return i*20;  
                }  
            };  
            assertEquals(20, StateMocked.getDouble(1));  
            assertEquals(40, StateMocked.getDouble(2));   
        }
    
下一节:JUnit是一个Java语言的单元测试框架。它由肯特·贝克和埃里希·伽玛(Erich Gamma)建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中为最成功的一个。