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>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.21</version> <scope>test</scope> </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通过数据库查询出不同分组货物的数量,得到货物是否畅销。
1 package com.khlin.test.junit.jmockit.demo; 2 3 public class Service { 4 5 private DAO dao; 6 7 public void setDao(DAO dao) { 8 this.dao = dao; 9 } 10 11 /** 12 * 根据存货量判断货物是否畅销 13 * @param group 14 * @return 15 */ 16 public Status checkStatus(String group) { 17 int count = this.dao.getStoreCount(group); 18 19 if (count <= 0) { 20 return Status.UNKOWN; 21 } else if (count <= 800) { 22 return Status.UNSALABLE; 23 } else if (count <= 1000) { 24 return Status.NORMAL; 25 } else { 26 return Status.SELLINGWELL; 27 } 28 } 29 } |
1 package com.khlin.test.junit.jmockit.demo; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 public class DAO { 7 8 private Map<String, Integer> groupCounts = new HashMap<String, Integer>(); 9 10 /** 11 * 假数据 12 */ 13 { 14 this.groupCounts.put("A", 500); 15 this.groupCounts.put("B", 1000); 16 this.groupCounts.put("C", 1200); 17 } 18 19 public int getStoreCount(String group) { 20 Integer count = this.groupCounts.get(group); 21 22 return null == count ? -1 : count.intValue(); 23 } 24 } |
1 package com.khlin.test.junit.jmockit.demo; 2 3 public enum Status { 4 5 /** 6 * 畅销 7 */ 8 SELLINGWELL, 9 /** 10 * 一般 11 */ 12 NORMAL, 13 /** 14 * 滞销 15 */ 16 UNSALABLE, 17 18 /** 19 * 状态未知 20 */ 21 UNKOWN 22 } |
基于行为的Mock 测试,一共三个阶段:record、replay、verify。
1)record:在这个阶段,各种在实际执行中期望被调用的方法都会被录制。
2)repaly:在这个阶段,执行单元测试Case,原先在record 阶段被录制的调用都可能有机会被执行到。这里有“有可能”强调了并不是录制了就一定会严格执行。
3)verify:在这个阶段,断言测试的执行结果或者其他是否是原来期望的那样。
假设现在我只想测试Service,在存货量900件的情况下,是否能正确返回NORMAL的状态。那么,我并不关心传入DAO的到底是哪个分组,也不关心DAO怎么去数据库取数,我只想让DAO返回900,这样就可以测试Service了。
示例代码:
1 @RunWith(JMockit.class) 2 public class ServiceBehavier { 3 4 @Mocked 5 DAO dao = new DAO(); 6 7 private Service service = new Service(); 8 9 @Test 10 public void test() { 11 12 // 1. record 录制期望值 13 new NonStrictExpectations() { 14 { 15 /** 16 * 录制的方法 17 */ 18 dao.getStoreCount(anyString);// mock这个方法,无论传入任何String类型的值,都返回同样的值,达到黑盒的效果 19 /** 20 * 预期结果,返回900 21 */ 22 result = 900; 23 /** 24 times必须调用两次。在Expectations中,必须调用,否则会报错,因此不需要作校验。 25 在NonStrictExpectations中不强制要求,但要进行verify验证.但似乎已经强制要求了 26 此外还有maxTimes,minTimes 27 */ 28 times = 1; 29 } 30 }; 31 service.setDao(dao); 32 33 // 2. replay 调用 34 Assert.assertEquals(Status.NORMAL, service.checkStatus("D")); 35 36 // Assert.assertEquals(Status.NORMAL, service.checkStatus("D")); 37 38 //3.校验是否只调用了一次。如果上面注释的语句再调一次,且把录制的times改为2,那么在验证阶段将会报错。 39 new Verifications() { 40 { 41 dao.getStoreCount(anyString); 42 times = 1; 43 } 44 }; 45 46 } 47 } |
基于状态的Mock测试
通过MockUp类,直接改写了mock类的代码逻辑,有点类似白盒测试。
1 public class ServiceState { 2 3 private DAO dao; 4 5 private Service service; 6 7 @Test 8 public void test() { 9 10 //1. mock对象 11 MockUp<DAO> mockUp = new MockUp<DAO>() { 12 13 @Mock 14 public int getStoreCount(String group) { 15 return 2000; 16 } 17 }; 18 19 //2. 获取实例 20 dao = mockUp.getMockInstance(); 21 service = new Service(); 22 service.setDao(dao); 23 24 //3.调用 25 Assert.assertEquals(Status.SELLINGWELL, service.checkStatus("FFF")); 26 27 //4. 还原对象,避免测试方法之间互相影响。其实对一个实例来说没什么影响,对静态方法影响较大。旧版本的tearDown()方法是Mockit类的静态方法 28 mockUp.tearDown(); 29 } 30 } |
3. JMockit mock各种类型或方法的示例代码
抽象类
1 package com.khlin.test.junit.jmockit.demo.jmockit; 2 3 public abstract class AbstractA { 4 5 public abstract int getAbstractAnything(); 6 7 public int getAnything() { 8 return 1; 9 } 10 } |
接口类
1 package com.khlin.test.junit.jmockit.demo.jmockit; 2 3 public interface InterfaceB { 4 5 public int getAnything(); 6 } |
普通类
1 package com.khlin.test.junit.jmockit.demo.jmockit; 2 3 public class ClassA { 4 5 InterfaceB interfaceB; 6 7 private int number; 8 9 public void setInterfaceB(InterfaceB interfaceB) { 10 this.interfaceB = interfaceB; 11 } 12 13 public int getAnything() { 14 return getAnythingPrivate(); 15 } 16 17 private int getAnythingPrivate() { 18 return 1; 19 } 20 21 public int getNumber() { 22 return number; 23 } 24 25 26 27 public static int getStaticAnything(){ 28 return getStaticAnythingPrivate(); 29 } 30 31 private static int getStaticAnythingPrivate() { 32 return 1; 33 } 34 35 public int getClassBAnything() { 36 return this.interfaceB.getAnything(); 37 } 38 } |
接口实现类
1 package com.khlin.test.junit.jmockit.demo.jmockit; 2 3 public class ClassB implements InterfaceB { 4 5 public int getAnything() { 6 return 10; 7 } 8 9 } |
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。