TDD 推求测试先行,不光在自己代码未实现时可以先做好测试,即使平台依赖或第三方接口未准备好我们也能先行一步的,这就要对接口依赖进行 Mock。同时 Mock 也使得我们的测试代码在运行当中不至于随着第三方接口的沦陷而坠入深渊。
Java 中 Mock 工具也不少,像通用 EasyMock, jMock, Mockito, Unitils Mock, PowerMock, 再比如偏专业的 HttpMock, StrutsMock 等。但 JMock 与前面各位相比简直是全能选手,对 final/static/native/private 方法都能 Mock,功能上还远不止这些了,可以看看一个对比图 https://code.google.com/p/jmockit/wiki/MockingToolkitComparisonMatrix。
JMockit 是基于 Java5 的 java.lang.instrument 包开发的,所以它才能夺得先机,也可陷得更深。自然它要求 JDK5 及以上,JUnit 4.8 及以上版本。命令行下原来用 -javaagent:/.../lib/jmockit.jar 加载 JMockit,现在发现把 jmockit.jar 放在 classpath 下就 OK 的,但是必须放在 junit.jar 包之前,否则你会看到这个 java.lang.IllegalStateException: JMockit wasn't properly initialized; check that jmockit.jar precedes junit.jar in the classpath。 JMockit 有两种 Mock 方式:
1. Behavior-oriented(Expectations & Verifications) --- 基于代码执行行为的模仿,象黑盒测试
2. State-oriented(MockUp) --- 侵入类内部,随意模仿,似白盒,可以说是能为所欲为
此篇体验下第一种 Mock 方式,在测试代码中最直观就是那个 new Expectations(...){{result = some;}},下面来看个实际的例子。应用场景是
MyService.testFetchData() 方法要调用 ExternalService.fetchDataFor() 方法来获得数据,那么我们在 ExternalService.fetchDataFor() 尚未实现之前怎么去测试 MyService.testFetchData() 方法呢,JMockit 要做的就是对 ExternalService.fetchDataFor() 的返回进行 Mock。
具体实现代码如下:
1. ExternalService.java
package cc.unmi;
public class ExternalService {
public static String fetchDataFor(String name){
System.out.println("call ExternalService.fetchDataFor");
throw new RuntimeException("Not implemented yet!");
}
}
1
2
3
4
5
6
7
8
packagecc.unmi;
publicclassExternalService{
publicstaticStringfetchDataFor(Stringname){
System.out.println("call ExternalService.fetchDataFor");
thrownewRuntimeException("Not implemented yet!");
}
}
方法 fetchDataFor 并未真正实现,如果测试代码真的进入到这个方法来总是会导致测试用例失败的,除非你就是想要来测试异常的,那没活说。
2. MyService.java
package cc.unmi;
public class MyService {
public static String fetchData(String name){
System.out.println("call MyService.fetchData");
return ExternalService.fetchDataFor(name);
}
}
1
2
3
4
5
6
7
8
packagecc.unmi;
publicclassMyService{
publicstaticStringfetchData(Stringname){
System.out.println("call MyService.fetchData");
returnExternalService.fetchDataFor(name);
}
}
3. MyServiceTest.java
package cc.unmi;
import mockit.Expectations;
import org.junit.*;
public class MyServiceTest {
@Test
public void testFetchData() {
new Expectations(ExternalService.class){
{
ExternalService.fetchDataFor("Unmi");
result = "blog: http://unmi.cc";
//上行或者用 returns("blog: http://unmi.cc")
}
};
String actual = MyService.fetchData("Unmi");
Assert.assertEquals("blog: http://unmi.cc", actual);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
packagecc.unmi;
importmockit.Expectations;
importorg.junit.*;
publicclassMyServiceTest{
@Test
publicvoidtestFetchData(){
newExpectations(ExternalService.class){
{
ExternalService.fetchDataFor("Unmi");
result="blog: http://unmi.cc";
//上行或者用 returns("blog: http://unmi.cc")
}
};
Stringactual=MyService.fetchData("Unmi");
Assert.assertEquals("blog: http://unmi.cc",actual);
}
}
上面的代码能给予我们的想像空间很大
new Expectations(...){{}} 产生了一个匿名子类和它的实例
对该实例的 result 属性的赋值就是预设的 ExternalService.fetchDataFor("Unmi") 的返回值
运行时参数的匹配的讲究,如果匹配不上也不会调用被 Mock 的方法,参数选择上可以有 anyString, withAny(obj) 等方式来忽略某个位置上的参数匹配
Expectations 是严格的,即被 Mock 方法必须被精确命中; 同时还有一个 NonStrictExpectations, 就是说我们 Mock 的方法只作壁上观也无妨
怎么同时 Mock 多个类,多个方法呢?
还有这里怎么 Mock 私有方法,怎么 Mock 实例方法呢?
为控制篇幅,有些问题其他地方再探讨吧。
我们在命令行下执行并看到输出:
unmi@localhost$ java -classpath lib/jmockit.jar:lib/junit-4.11.jar:lib/hamcrest-core-1.3.jar:bin org.junit.runner.JUnitCore cc.unmi.MyServiceTest
JUnit version 4.11
.call MyService.fetchData
Time: 0.029
OK (1 test)
测试通过,并未实际去调用 ExternalService.fetchDataFor("Unmi"),这个被 JMockit 接管了。
如果把 MyServiceTest.java 中 MyService.fetchData("Unmi") 改成 MyService.fetchData("Hello Unmi"), 再次跑下测试就失败了
.call MyService.fetchData
call ExternalService.fetchDataFor
E
Time: 0.031
There was 1 failure:
1) testFetchData(cc.unmi.MyServiceTest)
java.lang.RuntimeException: Not implemented yet!
at cc.unmi.ExternalService.fetchDataFor(ExternalService.java:6)
at cc.unmi.MyService.fetchData(MyService.java:6)
at cc.unmi.MyServiceTest.testFetchData(MyServiceTest.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:606)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:606)
FAILURES!!!
Tests run: 1, Failures: 1
参数匹配不上就跑实际方法实现去了,现在把 MyServiceTest.java 中的 ExternalService.fetchDataFor("Unmi"); 改为 ExternalService.fetchDataFor(anyString); 测试用例又可以通过。
略加延伸
Mock 实例方法时, Expectations 可以这么写:
new Expectations(){
@Mocked ExternalService service;
{
service.fetchDataFor("Unmi");
result = "all right";
}
};
1
2
3
4
5
6
7
newExpectations(){
@MockedExternalServiceservice;
{
service.fetchDataFor("Unmi");
result="all right";
}
};
因 为 Java 不强制调用 static 方法要用 Class,也可以用实例来调用 static 方法,所以上面的方式同样也适用于对 static 方法的 Mock,只是平白多出 service 实例来。基于对 new Expectations(){{...}} 的理解,MyServiceTest.java 可这么写:
public class MyServiceTest {
@Mocked ExternalService service;
@Test
public void testFetchData() {
new Expectations(service) {
{
service.fetchDataFor("Unmi");
result = "all right";
}
};
String actual = MyService.fetchData("Unmi");
Assert.assertEquals("all right", actual);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
publicclassMyServiceTest{
@MockedExternalServiceservice;
@Test
publicvoidtestFetchData(){
newExpectations(service){
{
service.fetchDataFor("Unmi");
result="all right";
}
};
Stringactual=MyService.fetchData("Unmi");
Assert.assertEquals("all right",actual);
}
}