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()); } }