微服务环境下的集成测试探索 —— 服务 Stub & Mock

Posted 企鹅杏仁技术站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务环境下的集成测试探索 —— 服务 Stub & Mock相关的知识,希望对你有一定的参考价值。


作者 | 章烨明

杏仁医生CTO。中老年程序员,关注各种技术和团队管理。

引子

现在微服务很流行,企业架构微服务化的确能解决不少问题,但是在微服务环境下,服务之间的依赖以及由此造成的开发、测试和集成的问题,一直都是微服务最大的痛点。

传统的解决方案是,除了测试、预发布和生产环境,还会部署多套用于开发和集成的环境。这样存在的问题是,只要有一组服务出现问题,就会影响其他使用该环境的团队的日常开发和测试。而且常常出现问题后,需要耗费很多时间定位,结果还常常是因为某个服务的版本没有同步。并且多套环境维护起来也是一个麻烦重重,即使有了容器。

这次我们一起来探索一下 API 模拟工具以及基于契约的测试,也许会是解决这个问题的一个方案。

WireMock 介绍

我们开发应用也好、服务也好,常常需要依赖后端或者服务的接口。例如开发移动应用 App,可能后端接口还在开发中,这时 App 的开发因为无法调用后端,很不方便。又或者程序会依赖第三方的接口,例如微信支付,在本地开发时不能直接调用。

这时我们就会需要一个工具来模拟这些服务,WireMock 就是这样的一个工具,主要针对的是最常见的 HTTP 服务。

WireMock 用于开发调试

WireMock 首先自身就是一个可以独立运行的服务。下载 Standalone Jar 文件后,即可可以直接运行。

java -jar wiremock-standalone-2.11.0.jar

此时可以通过 Json 映射文件来定义 Stub 服务。例如下面是一个映射文件,request 部分设置匹配的 Url 路径、请求方法及参数,如果匹配到了,则会返回 response 部分设置的内容。把该文件放到 WireMock 同路径下的 mappings目录下即可。

{
 "request" : { "urlPath" : "/api/order/find", "method" : "GET", "queryParameters" : { "orderId" : { "matches" : "^[0-9]{16}$" } } }, "response" : { "status" : 200, "bodyFileName" : "body-order-find-1.json", "headers" : { "Content-Type" : "application/json;charset=UTF-8" } }
}

Response 的内容可以直接在映射文件里设置,也可以引用了另一个文件。这里是引用了一个名为 body-order-find-1.json 的文件,该文件放置在 WireMock 同路径下的 __files 目录下。

{
   "success": true, "data": { "id": 781202, "buyerId": -2, "status": 0, // 略... }
}

下面我们用 curl 测试一下。第一次我们请求的参数 orderId 无法匹配指定的正则,WireMock 会返回 Request was not matched,而且还会很贴心的告诉你最接近的匹配是什么。


$ curl http://localhost:8080/api/order/find?orderId=abcdefghijklmnop
           Request was not matched
           =======================

----------------------------------------------
| Closest stub         | Request 
----------------------------------------------
GET                    | GET
/api/order/find        | /api/order/find
----------------------------------------------

第二次我们参数 orderId 匹配的话,WireMock 会直接返回设置的结果。

$ curl http://localhost:8080/api/order/find?orderId=9999999999999999
{
    "success": true,
    "data": {
        "id": 781202,
        "buyerId": -2,
        "status": 0
    }
}

上面的例子是 WireMock 最基本的用法,除了请求匹配响应,WireMock 也能支持:

  • 通过 RESTFul 的接口提交和管理请求映射和相应。

  • 支持响应模板,返回内容时会将变量填充到响应模板中。当然,这里的模板功能是比较简单的,但对于大部分 Stub 的场景应该是足够了。

  • 支持模拟异常返回,例如设置有一定比例的超时返回等等,这个功能用于测试非常方便。

为了方便编写请求映射文件,WireMock 还可以运行在代理模式,只需要运行时添加 --enable-browser-proxying 参数即可。此时 WireMock 匹配到请求后,不是返回指定的内容,而是把请求 Forword 到指定的 URL,获得 Response 后再返回给调用方。同时,WireMock 会记录请求和返回的内容,生成 Json 映射文件。使用时只要根据需求对这些映射文件做一定修改,既可以用来模拟目标服务。

WireMock 用于集成测试

除了独立运行,WireMock 也可以直接嵌入到代码中。最方便的就是在 JUnit 中使用,WireMock 提供了 WireMockRule, 可以很方便的在测试时嵌入一个 Stub 服务。

下面是一个支付相关的集成测试,被测试方法会调用微信的支付服务。stubForUnifiedOrderSuccess 设置了一个很简单的 Stub,一旦匹配到请求的 URL 为 /pay/unifiedorder,那就返回指定的 XML 内容。这样我就可以在集成测试里测试整个支付流程,而不必依赖真正的微信支付。当然,测试时微信支付接口的 Host 也要改成 WireMockRule 配置的本地端口。并且,通过这种方式也很容易测试一些异常情况,根据需要修改 Stub 返回的内容即可。

public class OrderTest {
   @Rule public WireMockRule wireMockRule = new WireMockRule(9090); /**    
    * 统一下单 Stub
   
    * 参考 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
   
    *
   
    * @param tradeType 交易类型, 可以是JSAPI、NATIVE或APP
   
    */
public void stubForUnifiedOrderSuccess(String tradeType) { String unifiedOrderResp = "<xml>\n" + "    <return_code><![CDATA[SUCCESS]]></return_code>\n" + "    <return_msg><![CDATA[OK]]></return_msg>\n" + "    <appid><![CDATA[wxxxxxxxxxxxxxxxxx]]></appid>\n" + "    <mch_id><![CDATA[9999999999]]></mch_id>\n" + "    ...... \n" + "    <trade_type><![CDATA[" + tradeType + "]]></trade_type>\n" + "</xml>"; stubFor(post(urlEqualTo("/pay/unifiedorder")) .withHeader("Content-Type", equalTo("text/xml;charset=UTF-8")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "text/plain") .withBody(unifiedOrderResp))); } @Test public void test001_doPay() { stubForUnifiedOrderSuccess("JSAPI"); payServices.pay(); // 测试代码... }
}

有时候在集成测试里,我们还需要验证系统的行为,例如是否调用了某个 API,调用了几次,调用的参数和内容是否符合要求等。区别于前面说的 Stub,其实这就是常说的 Mock 功能。WireMock 对此也有很强大的支持。

verify(postRequestedFor(urlEqualTo("/pay/unifiedorder"))
       .withHeader("Content-Type", equalTo("text/xml;charset=UTF-8"))
.withQueryParam("param", equalTo("param1")) .withRequestBody(containing("success"));

这样,有了 WireMock,集成测试时处理第三方的依赖就非常方便了。不需要直接调用依赖的服务,也不需要专门创建用于集成测试的 Stub 或 Mock,直接代码中根据需要设置即可。

WireMock 总结

总结一下, WireMock 可以:

  • 作为代理运行,此时可以录制请求和返回的脚本,用于后继 Stub 和 Mock 使用。

  • 独立运行,作为一个 Stub 服务,根据匹配的请求返回数据。

  • 作为 Stub,通过代码嵌入 HTTP 模拟服务,在指定端口监听,并根据匹配的请求返回数据。

  • 作为 Mock,在单元测试和集成测试中,验证请求逻辑。例如是否进行了调用、参数是否正确等。

这里再强调下 Stub 和 Mock 的区别,很多人经常搞混。Stub 就是一个纯粹的模拟服务,用于替代真实的服务,收到请求返回指定结果,不会记录任何信息。Mock 则更进一步,还会记录调用行为,可以根据行为来验证系统的正确性。

我们可以用 WireMock 来优化开发和集成的流程。

  • 在外部服务尚未开发完成时,模拟服务,方便开发。

  • 在本地开发时,模拟外部服务避免直接依赖。

  • 在单元测试中模拟外部服务,同时验证业务逻辑。

契约式测试

本文主要以 WireMock 为例介绍了 API 模拟工具的使用方法。其实除了 WireMock,还有不少类似的工具,例如最早的 MounteBank,以及 MockServer、Moco 等也都是很强大的工具。

不过,在微服务环境下,光有 API 模拟工具还不够。对于 WireMock,首先必须考虑如何来管理大量的映射文件。一个方法是开发一个专用的 Stub 平台,来管理所有的映射文件,同时作为 Stub 运行。另外一个方法是通过 Git 来管理映射文件,需要的时候同步下来运行 WireMock 即可。

另外,我们上面提到 WireMock 的两大作用,调用方模拟服务以及服务方集成测试,是否可以统一两者呢?也就是说,调用方和服务方约定好接口,生成映射文件,这个文件即可以用于客户端模拟服务,也可以用于服务方集成测试,这样双方开发也好、集成也好都会方便很多。下一篇我们来研究一下 Spring Cloud Contract,它就是基于 WireMock 实现了契约式的测试,上文中双方约定好的接口,其实就是双方的契约。

 全文完


以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。


杏仁技术站




以上是关于微服务环境下的集成测试探索 —— 服务 Stub & Mock的主要内容,如果未能解决你的问题,请参考以下文章

微服务集成测试自动化探索

51信用卡的微服务集成测试自动化探索

微服务下的单元测试和集成测试

美团点评:打造微服务自动化测试与持续集成工具链实践

探索解析微服务下的RabbitMQ

微服务架构下的开发部署