最近我创建并维护了一个开源项目 http-api-invoker ,该项目实现将 HTTP 请求和接口进行绑定,让 HTTP 接口调用跟调用本地方法一样自然优雅。在写单元测试的时候,因为需要发送 HTTP 请求,而做为一个完整独立的项目,我并不希望对它进行单测还需要依赖其他的项目。最开始我用的是 Mockito。
为了让代码更易于测试,我将真正发送请求的任务交给一个接口(Requestor),然后写了一个默认的实现类,用于发送请求。当需要测试的时候,Mock一个Requestor,于是所有请求并没有真正地发出去,只需要断言这个Mock出来的Requstor发送请求的方法有没有被正确调用就可以。
这个是使用 Mockito 的情况下,我能想到的最好的解决方案了。
但是,这里面有一个问题。默认的 Requestor 实现又如何被独立测试呢? 这让我犯难了,所以项目刚开始的时候并没有对默认的 Requestor 进行单元测试,也没有测试真正发送请求的情况下,代码的逻辑是否正确。
不久前的某个深夜,我偶然间看到一篇 InfoQ 的上的文章 Stubbing, Mocking and Service Virtualization Differences for Test and Development Teams 让我充分了解了Mock、打桩和模拟服务的区别和应用场景,受益匪浅。在文章里面介绍了 wiremock 这个框架,于是我找到官网 ,看了一下,文档非常地清晰和完善。感觉如获至宝。
我的项目刚好最需要这样的框架来做单元测试。于是我动手写了测试样例。因为官网有非常详细的文档,而且也查看了一些博客上的入门样例,很快就上手了。深深感觉到它的强大,真的非常振奋人心。具体代码可以查看 CityServiceTest 这个测试类。
<dependency>
<groupId>com.github.tomakehurstgroupId>
<artifactId>wiremock-standaloneartifactId>
<version>2.19.0version>
<scope>testscope>
dependency>
我这里写了一个简单的测试用例,完整的真实项目用例可以查看 CityServiceTest
public class HelloWireMockTest {
private static final int PORT = 18888;
/**
* 使用给定的端口号生成WireMockRule实例.
* 这里设置之后,启动测试时 WireMock 会使用内嵌的 Jetty 启动一个 Web 服务器并监听指定的端口
*/
@Rule
public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
@Test
public void helloTest() {
String uri = "/say/hello";
String body = "Hello World!";
// 给uri打桩,这个语句表示,拦截 uri 为 /say/hello 的请求回复 "Hello World!"
wireMockRule.stubFor(get(urlEqualTo(uri))
.willReturn(aResponse().withBody(body)));
// 这里我们可以用任何方式发起HTTP的GET请求
String result = HttpUtil.get("http://localhost:" + PORT + uri);
System.out.println(result);
// 断言我们发出的请求返回了我们期望的结果
assertEquals(result, body);
}
这个测试用例跑起来的时候,WireMock会启动一个Web服务器监听 18888 端口,然后我们预设 /say/hello 接口将返回 Hello World! 这行文本做为响应,接下来我们发一个请求过去,断言拿到的是我们期望的结果。
更强大的是,当发的请求跟我们预设的不匹配的时候,它会明明白白地告诉我们,差异在哪里,例如:
而且它也支持 header 和 cookie 的验证,例如
@Test
public void getCityWithHeaders() {
Map<String, String> headers = new HashMap<>();
String key = "auth";
String key2 = "auth2";
headers.put(key, "123");
headers.put(key2, "321");
int id = 1;
String uri = "/city/getCityRest/" + id;
City mockCity = createCity(id);
wireMockRule.stubFor(get(urlEqualTo(uri))
// 声明我们的请求必须包含两个指定的 header
.withHeader(key, equalTo(headers.get(key)))
.withHeader(key2, equalTo(headers.get(key2)))
// 一旦有符合要求的请求过来,则返回指定的响应
.willReturn(aResponse().withBody(JSON.toJSONString(mockCity))));
// 使用 http-api-invoker 框架,只需调用接口的方法,框架会发送相应的 http 请求
// 这里 cityService.getCityWithHeaders 方法我们绑定的地址是 /getCityRest/{id}
City result = cityService.getCityWithHeaders(id, headers);
assertEquals(mockCity, result);
}
如果我们发请求的时候,header 没有带上,那么控制台就会打印出下面这报告:
单元测试对于写出健壮且高质量的代码非常有必要。没有单测,开发的时候就像夜里一个人走在没有灯的荒郊野岭,你永远不知道前面等待你的是小坑还是深渊,有时候出了问题自己怎么死的都不知道。而单元测试为这场野外旅行添置了一盏明灯,照亮前面的路,让你每走几步都能知道现在处在什么位置。就算掉坑里,也马上让你知道你现在在坑里,赶紧出来,以防止更糟糕的情况。
一个好的单元测试用例也有很大的学问,我相信,花一些时间学习和了解这些技能是一本万利的事情。与君共勉。