使用JUnit4与JMockit进行打桩测试

1. 何为Mock

项目中各个模块,各个类之间会有互相依赖的关系,在单元测试中,我们只关心被测试的单元,对于其依赖的单元并不关心(会有另外针对该单元的测试)。

比如,逻辑层A类依赖了数据访问层B类的取数方法,然后进行逻辑处理。在对A的单元测试中,我们关注的是在B返回不同的查询结果的时候,A是怎么处理的,而不是B到底是怎么取的数,如何封装成一个模型等等。

因此,要屏蔽掉这些外部依赖,而Mock让我们有了一套仿真的环境。

目前业界有几种Mock,这里选用最全面的JMockit进行总结。

2. JMockit简介

JMockit的工作原理是通过asm修改原有class的字节码,再利用jdk的instrument机制替换现有class的内容,从而达到mock的目的。

这里使用的JMockit是1.21版本,具体使用方法可能与其他版本的不一样,但思想是相通的。Maven 配置如下:

<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
<dependency>
  <groupId>org.jmockit</groupId> 
  <artifactId>jmockit</artifactId> 
  <version>1.21</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.jacoco</groupId>
  <artifactId>org.jacoco.ant</artifactId>
  <version>0.8.4</version>
</dependency>

<dependency>
  <groupId>org.jacoco</groupId>
  <artifactId>org.jacoco.agent</artifactId>
  <version>0.8.4</version>
</dependency>

<dependency>
  <groupId>org.jacoco</groupId>
  <artifactId>org.jacoco.report</artifactId>
  <version>0.8.4</version>
</dependency>
<dependency>
  <groupId>org.jacoco</groupId>
  <artifactId>org.jacoco.core</artifactId>
  <version>0.8.4</version>
</dependency>

如果需要使用jmock1.0版本,则maven配置如下 新版去废除了一些功能(如@Mocked不能修饰成员)

<dependency>

            <groupId>junit</groupId>

            <artifactId>junit</artifactId>

            <version>4.11</version>

            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>com.googlecode.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>1.0</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.ant</artifactId>
            <version>0.8.4</version>
        </dependency>

        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <version>0.8.4</version>
        </dependency>

        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.report</artifactId>
            <version>0.8.4</version>
        </dependency>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.core</artifactId>
            <version>0.8.4</version>
        </dependency>

JMockit有两种测试方式,一种是基于行为的,一种是基于状态的测试。

1) Behavior-oriented(Expectations & Verifications)  

2)State-oriented(MockUp<GenericType>)   

通俗点讲,Behavior-oriented是基于行为的mock,对mock目标代码的行为进行模仿,更像黑盒测试。State-oriented 是基于状态的mock,是站在目标测试代码内部的。可以对传入的参数进行检查、匹配,才返回某些结果,类似白盒。而State-oriented的 new MockUp基本上可以mock任何代码或逻辑。

假设现在有两个类,Service和DAO.  Service通过数据库查询出不同分组货物的数量,得到货物是否畅销。

package com.khlin.test.junit.jmockit.demo;

public class Service {
    
    private DAO dao;
    
    public void setDao(DAO dao) {
        this.dao = dao;
    }
    
    /**
     * 根据存货量判断货物是否畅销
     * @param group
     * @return
     */
    public Status checkStatus(String group) {
        int count = this.dao.getStoreCount(group);

        if (count <= 0) {
            return Status.UNKOWN;
        } else if (count <= 800) {
            return Status.UNSALABLE;
        } else if (count <= 1000) {
            return Status.NORMAL;
        } else {
            return Status.SELLINGWELL;
        }
    }
}


package com.khlin.test.junit.jmockit.demo;

import java.util.HashMap;
import java.util.Map;

public class DAO {

    private Map<String, Integer> groupCounts = new HashMap<String, Integer>();

    /**
     * 假数据
     */
    {
        this.groupCounts.put("A", 500);
        this.groupCounts.put("B", 1000);
        this.groupCounts.put("C", 1200);
    }

    public int getStoreCount(String group) {
        Integer count = this.groupCounts.get(group);

        return null == count ? -1 : count.intValue();
    }
}
package com.khlin.test.junit.jmockit.demo;

public enum Status {

    /**
     * 畅销
     */
    SELLINGWELL,
    /**
     * 一般
     */
    NORMAL,
    /**
     * 滞销
     */
    UNSALABLE,
    
    /**
     * 状态未知
     */
    UNKOWN
}

基于行为的Mock 测试,一共三个阶段:record、replay、verify。

1)record:在这个阶段,各种在实际执行中期望被调用的方法都会被录制。

2)repaly:在这个阶段,执行单元测试Case,原先在record 阶段被录制的调用都可能有机会被执行到。这里有“有可能”强调了并不是录制了就一定会严格执行。

3)verify:在这个阶段,断言测试的执行结果或者其他是否是原来期望的那样。

假设现在我只想测试Service,在存货量900件的情况下,是否能正确返回NORMAL的状态。那么,我并不关心传入DAO的到底是哪个分组,也不关心DAO怎么去数据库取数,我只想让DAO返回900,这样就可以测试Service了。

示例代码:

@RunWith(JMockit.class)
public class ServiceBehavier {

    @Mocked
    DAO dao = new DAO();

    private Service service = new Service();

    @Test
    public void test() {

        // 1. record 录制期望值
        new NonStrictExpectations() {
            {
                /**
                 * 录制的方法
                 */
                dao.getStoreCount(anyString);// mock这个方法,无论传入任何String类型的值,都返回同样的值,达到黑盒的效果
                /**
                 * 预期结果,返回900
                 */
                result = 900;
                /**
                times必须调用两次。在Expectations中,必须调用,否则会报错,因此不需要作校验。
                在NonStrictExpectations中不强制要求,但要进行verify验证.但似乎已经强制要求了
                此外还有maxTimes,minTimes
                */
                times = 1;
            }
        };
        service.setDao(dao);

        // 2. replay 调用
        Assert.assertEquals(Status.NORMAL, service.checkStatus("D"));

//        Assert.assertEquals(Status.NORMAL, service.checkStatus("D"));

         //3.校验是否只调用了一次。如果上面注释的语句再调一次,且把录制的times改为2,那么在验证阶段将会报错。
        new Verifications() {
            {
                dao.getStoreCount(anyString);
                times = 1;
            }
        };

    }
}

基于状态的Mock测试

通过MockUp类,直接改写了mock类的代码逻辑,有点类似白盒测试。

public class ServiceState {

    private DAO dao;
    
    private Service service;

    @Test
    public void test() {
        
        //1. mock对象
        MockUp<DAO> mockUp = new MockUp<DAO>() {

            @Mock
            public int getStoreCount(String group) {
                return 2000;
            }
        };
        
        //2. 获取实例
        dao = mockUp.getMockInstance();
        service = new Service();
        service.setDao(dao);
        
        //3.调用
        Assert.assertEquals(Status.SELLINGWELL, service.checkStatus("FFF"));
        
        //4. 还原对象,避免测试方法之间互相影响。其实对一个实例来说没什么影响,对静态方法影响较大。旧版本的tearDown()方法是Mockit类的静态方法
        mockUp.tearDown();
    }
}

3. JMockit mock各种类型或方法的示例代码

抽象类 

package com.khlin.test.junit.jmockit.demo.jmockit;

public abstract class AbstractA {
    
    public abstract int getAbstractAnything();

    public int getAnything() {
        return 1;
    }
}

接口类

package com.khlin.test.junit.jmockit.demo.jmockit;

public interface InterfaceB {
    
    public int getAnything();
}

普通类

package com.khlin.test.junit.jmockit.demo.jmockit;

public class ClassA {
    
    InterfaceB interfaceB;
    
    private int number;
    
    public void setInterfaceB(InterfaceB interfaceB) {
        this.interfaceB = interfaceB;
    }
    
    public int getAnything() {
        return getAnythingPrivate();
    }
    
    private int getAnythingPrivate() {
        return 1;
    }
    
    public int getNumber() {
        return number;
    }
    
    
    
    public static int getStaticAnything(){
        return getStaticAnythingPrivate();
    }
    
    private static int getStaticAnythingPrivate() {
        return 1;
    }
    
    public int getClassBAnything() {
        return this.interfaceB.getAnything();
    }
}

接口实现类

package com.khlin.test.junit.jmockit.demo.jmockit;

public class ClassB implements InterfaceB {

    public int getAnything() {
        return 10;
    }
    
}

终极测试代码

package com.khlin.test.junit.jmockit.demo;

import mockit.Deencapsulation;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Mock;
import mockit.MockUp;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.Tested;
import mockit.Verifications;
import mockit.integration.junit4.JMockit;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.khlin.test.junit.jmockit.demo.jmockit.AbstractA;
import com.khlin.test.junit.jmockit.demo.jmockit.ClassA;
import com.khlin.test.junit.jmockit.demo.jmockit.ClassB;
import com.khlin.test.junit.jmockit.demo.jmockit.InterfaceB;

@RunWith(JMockit.class)
public class JMockitTest {

    /**
     * mock私有方法
     */
    @Test
    public void testPrivateMethod() {

        final ClassA a = new ClassA();
        // 局部参数,把a传进去
        new NonStrictExpectations(a) {
            {
                Deencapsulation.invoke(a, "getAnythingPrivate");
                result = 100;
                times = 1;
            }
        };

        Assert.assertEquals(100, a.getAnything());

        new Verifications() {
            {
                Deencapsulation.invoke(a, "getAnythingPrivate");
                times = 1;
            }
        };
    }

    /**
     * mock私有静态方法
     */
    @Test
    public void testPrivateStaticMethod() {

        new NonStrictExpectations(ClassA.class) {
            {
                Deencapsulation
                        .invoke(ClassA.class, "getStaticAnythingPrivate");
                result = 100;
                times = 1;
            }
        };

        Assert.assertEquals(100, ClassA.getStaticAnything());

        new Verifications() {
            {
                Deencapsulation
                        .invoke(ClassA.class, "getStaticAnythingPrivate");
                times = 1;
            }
        };

    }

    /**
     * mock公有方法
     */
    @Test
    public void testPublicMethod() {
        final ClassA classA = new ClassA();
        new NonStrictExpectations(classA) {
            {
                classA.getAnything();
                result = 100;
                times = 1;
            }
        };

        Assert.assertEquals(100, classA.getAnything());

        new Verifications() {
            {
                classA.getAnything();
                times = 1;
            }
        };
    }

    /**
     * mock公有静态方法--基于行为
     */
    @Test
    public void testPublicStaticMethod() {

        new NonStrictExpectations(ClassA.class) {
            {
                ClassA.getStaticAnything();
                result = 100;
                times = 1;
            }
        };

        Assert.assertEquals(100, ClassA.getStaticAnything());

        new Verifications() {
            {
                ClassA.getStaticAnything();
                times = 1;
            }
        };
    }
    
    /**
     * mock公有静态方法--基于状态
     */
    @Test
    public void testPublicStaticMethodBaseOnStatus() {

        MockUp<ClassA> mockUp = new MockUp<ClassA>() {
            @Mock
            public int getStaticAnything() { //注意这里不用声明为static
                return 100;
            }
        };
        
        Assert.assertEquals(100, ClassA.getStaticAnything());
    }
    
    /**
     * mock接口
     */
    @Test
    public void testInterface() {
        
        InterfaceB interfaceB = new MockUp<InterfaceB>() {
            @Mock
            public int getAnything() {
                return 100;
            }
        }.getMockInstance();
        
        
        ClassA classA = new ClassA();
        classA.setInterfaceB(interfaceB);
        
        Assert.assertEquals(100, classA.getClassBAnything());
    }
    
    /**
     * mock接口--基于状态
     */
    @Test
    public void testInterfaceBasedOnStatus() {
        final InterfaceB interfaceB = new ClassB();
        
        new NonStrictExpectations(interfaceB) {
            {
                interfaceB.getAnything();
                result = 100;
                times = 1;
            }
        };
        
        ClassA classA = new ClassA();
        classA.setInterfaceB(interfaceB);
        
        Assert.assertEquals(100, classA.getClassBAnything());
        
        new Verifications() {
            {
                interfaceB.getAnything();
                times = 1;
            }
        };
    }
    
    
    
    /**
     * mock抽象类
     */
    @Test
    public void testAbstract() {
        AbstractA abstractA = new MockUp<AbstractA>() {
            @Mock
            public int getAbstractAnything(){
                return 100;
            }
            
            @Mock
            public int getAnything(){
                return 1000;
            }
        }.getMockInstance();
        
        Assert.assertEquals(100, abstractA.getAbstractAnything());
        
        Assert.assertEquals(1000, abstractA.getAnything());
    }
}
原文地址:https://www.cnblogs.com/wsy0202/p/11374123.html