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 }
终极测试代码
1 package com.khlin.test.junit.jmockit.demo;
2
3 import mockit.Deencapsulation;
4 import mockit.Expectations;
5 import mockit.Injectable;
6 import mockit.Mock;
7 import mockit.MockUp;
8 import mockit.Mocked;
9 import mockit.NonStrictExpectations;
10 import mockit.Tested;
11 import mockit.Verifications;
12 import mockit.integration.junit4.JMockit;
13
14 import org.junit.Assert;
15 import org.junit.Test;
16 import org.junit.runner.RunWith;
17
18 import com.khlin.test.junit.jmockit.demo.jmockit.AbstractA;
19 import com.khlin.test.junit.jmockit.demo.jmockit.ClassA;
20 import com.khlin.test.junit.jmockit.demo.jmockit.ClassB;
21 import com.khlin.test.junit.jmockit.demo.jmockit.InterfaceB;
22
23 @RunWith(JMockit.class)
24 public class JMockitTest {
25
26 /**
27 * mock私有方法
28 */
29 @Test
30 public void testPrivateMethod() {
31
32 final ClassA a = new ClassA();
33 // 局部参数,把a传进去
34 new NonStrictExpectations(a) {
35 {
36 Deencapsulation.invoke(a, "getAnythingPrivate");
37 result = 100;
38 times = 1;
39 }
40 };
41
42 Assert.assertEquals(100, a.getAnything());
43
44 new Verifications() {
45 {
46 Deencapsulation.invoke(a, "getAnythingPrivate");
47 times = 1;
48 }
49 };
50 }
51
52 /**
53 * mock私有静态方法
54 */
55 @Test
56 public void testPrivateStaticMethod() {
57
58 new NonStrictExpectations(ClassA.class) {
59 {
60 Deencapsulation
61 .invoke(ClassA.class, "getStaticAnythingPrivate");
62 result = 100;
63 times = 1;
64 }
65 };
66
67 Assert.assertEquals(100, ClassA.getStaticAnything());
68
69 new Verifications() {
70 {
71 Deencapsulation
72 .invoke(ClassA.class, "getStaticAnythingPrivate");
73 times = 1;
74 }
75 };
76
77 }
78
79 /**
80 * mock公有方法
81 */
82 @Test
83 public void testPublicMethod() {
84 final ClassA classA = new ClassA();
85 new NonStrictExpectations(classA) {
86 {
87 classA.getAnything();
88 result = 100;
89 times = 1;
90 }
91 };
92
93 Assert.assertEquals(100, classA.getAnything());
94
95 new Verifications() {
96 {
97 classA.getAnything();
98 times = 1;
99 }
100 };
101 }
102
103 /**
104 * mock公有静态方法--基于行为
105 */
106 @Test
107 public void testPublicStaticMethod() {
108
109 new NonStrictExpectations(ClassA.class) {
110 {
111 ClassA.getStaticAnything();
112 result = 100;
113 times = 1;
114 }
115 };
116
117 Assert.assertEquals(100, ClassA.getStaticAnything());
118
119 new Verifications() {
120 {
121 ClassA.getStaticAnything();
122 times = 1;
123 }
124 };
125 }
126
127 /**
128 * mock公有静态方法--基于状态
129 */
130 @Test
131 public void testPublicStaticMethodBaseOnStatus() {
132
133 MockUp<ClassA> mockUp = new MockUp<ClassA>() {
134 @Mock
135 public int getStaticAnything() { //注意这里不用声明为static
136 return 100;
137 }
138 };
139
140 Assert.assertEquals(100, ClassA.getStaticAnything());
141 }
142
143 /**
144 * mock接口
145 */
146 @Test
147 public void testInterface() {
148
149 InterfaceB interfaceB = new MockUp<InterfaceB>() {
150 @Mock
151 public int getAnything() {
152 return 100;
153 }
154 }.getMockInstance();
155
156
157 ClassA classA = new ClassA();
158 classA.setInterfaceB(interfaceB);
159
160 Assert.assertEquals(100, classA.getClassBAnything());
161 }
162
163 /**
164 * mock接口--基于状态
165 */
166 @Test
167 public void testInterfaceBasedOnStatus() {
168 final InterfaceB interfaceB = new ClassB();
169
170 new NonStrictExpectations(interfaceB) {
171 {
172 interfaceB.getAnything();
173 result = 100;
174 times = 1;
175 }
176 };
177
178 ClassA classA = new ClassA();
179 classA.setInterfaceB(interfaceB);
180
181 Assert.assertEquals(100, classA.getClassBAnything());
182
183 new Verifications() {
184 {
185 interfaceB.getAnything();
186 times = 1;
187 }
188 };
189 }
190
191
192
193 /**
194 * mock抽象类
195 */
196 @Test
197 public void testAbstract() {
198 AbstractA abstractA = new MockUp<AbstractA>() {
199 @Mock
200 public int getAbstractAnything(){
201 return 100;
202 }
203
204 @Mock
205 public int getAnything(){
206 return 1000;
207 }
208 }.getMockInstance();
209
210 Assert.assertEquals(100, abstractA.getAbstractAnything());
211
212 Assert.assertEquals(1000, abstractA.getAnything());
213 }
214 }