WireMock初体验,一个强大的HTTP 请求模拟测试框架

缘起

最近我创建并维护了一个开源项目 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 这个测试类。

使用入门

引入 maven 依赖

<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! 这行文本做为响应,接下来我们发一个请求过去,断言拿到的是我们期望的结果。

更强大的是,当发的请求跟我们预设的不匹配的时候,它会明明白白地告诉我们,差异在哪里,例如:
WireMock初体验,一个强大的HTTP 请求模拟测试框架_第1张图片

而且它也支持 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 没有带上,那么控制台就会打印出下面这报告:
WireMock初体验,一个强大的HTTP 请求模拟测试框架_第2张图片

结语

单元测试对于写出健壮且高质量的代码非常有必要。没有单测,开发的时候就像夜里一个人走在没有灯的荒郊野岭,你永远不知道前面等待你的是小坑还是深渊,有时候出了问题自己怎么死的都不知道。而单元测试为这场野外旅行添置了一盏明灯,照亮前面的路,让你每走几步都能知道现在处在什么位置。就算掉坑里,也马上让你知道你现在在坑里,赶紧出来,以防止更糟糕的情况。

一个好的单元测试用例也有很大的学问,我相信,花一些时间学习和了解这些技能是一本万利的事情。与君共勉。

你可能感兴趣的:(Java,单元测试,Mock,框架,Java单元测试实战)