微服务接口测试WireMock与契约测试CDC上篇咱们用RestAssured搞定了单体应用的接口测试。但微服务架构下你的服务依赖一堆下游服务怎么测今天聊WireMock模拟和契约测试这是微服务测试的两大杀器。一、微服务测试的困境假设你在开发订单服务它依赖这些下游┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 用户服务 │ │ 库存服务 │ │ 支付服务 │ │ user-svc │ │ stock-svc │ │ pay-svc │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ └───────────────────┼───────────────────┘ │ ┌──────┴──────┐ │ 订单服务 │ │ order-svc │ └─────────────┘问题1下游服务没就绪你在开发订单服务的创建订单功能但库存服务还在开发中。怎么办等他们做完问题2下游服务不稳定测试环境库存服务经常挂你的测试也跟着红一片。到底是你的bug还是他们的锅问题3接口变更没人通知支付服务改了响应字段从paymentStatus变成了status。你的代码没改线上直接炸。这三个问题WireMock 契约测试能一起解决。二、WireMock你的替身演员WireMock的核心思想在测试时启动一个假的HTTP服务模拟下游的响应。快速上手dependencygroupIdcom.github.tomakehurst/groupIdartifactIdwiremock-jre8/artifactIdversion2.35.0/versionscopetest/scope/dependency场景订单服务依赖库存服务ExtendWith(SpringExtension.class)SpringBootTest(webEnvironmentSpringBootTest.WebEnvironment.RANDOM_PORT)classOrderServiceIntegrationTest{LocalServerPortprivateintorderServicePort;// 启动WireMock服务器模拟库存服务RegisterExtensionstaticWireMockExtensionstockServiceMockWireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();AutowiredprivateOrderServiceorderService;DynamicPropertySourcestaticvoidconfigureProperties(DynamicPropertyRegistryregistry){// 让订单服务连上WireMock而不是真实的库存服务registry.add(inventory.service.url,stockServiceMock::baseUrl);}TestDisplayName(库存充足时订单创建成功)voidshouldCreateOrderWhenStockSufficient(){// 配置WireMock当收到库存查询请求时返回有库存stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).willReturn(aResponse().withStatus(200).withHeader(Content-Type,application/json).withBody( { sku: ITEM-001, availableQuantity: 100, reservedQuantity: 5 } )));// 配置扣减库存的mock响应stockServiceMock.stubFor(post(urlPathEqualTo(/api/stock/deduct)).withRequestBody(containing(ITEM-001)).willReturn(aResponse().withStatus(200).withBody( { success: true, deductedQuantity: 2, remainingQuantity: 98 } )));// 执行测试OrderRequestrequestnewOrderRequest(ITEM-001,2);OrderResultresultorderService.createOrder(request);// 验证assertThat(result.isSuccess()).isTrue();assertThat(result.getOrderId()).isNotNull();// 验证订单服务确实调了库存服务的扣减接口stockServiceMock.verify(postRequestedFor(urlPathEqualTo(/api/stock/deduct)).withRequestBody(containing(ITEM-001)));}TestDisplayName(库存不足时订单创建失败)voidshouldFailWhenStockInsufficient(){// 模拟库存不足stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).willReturn(aResponse().withStatus(200).withBody( { sku: ITEM-001, availableQuantity: 1, reservedQuantity: 0 } )));OrderRequestrequestnewOrderRequest(ITEM-001,5);// 要买5个但只有1个// 期望抛出库存不足异常assertThatThrownBy(()-orderService.createOrder(request)).isInstanceOf(InsufficientStockException.class).hasMessageContaining(库存不足);}}WireMock 的核心API速查// 匹配请求get(urlEqualTo(/api/users/1))get(urlPathEqualTo(/api/users/1))get(urlPathMatching(/api/users/\\d))post(urlEqualTo(/api/orders)).withHeader(Content-Type,containing(json)).withRequestBody(equalToJson({\name\:\Alice\}))// 构造响应willReturn(aResponse().withStatus(200).withHeader(Content-Type,application/json).withBody({\id\:1}).withFixedDelay(500))// 模拟延迟500ms// 验证请求是否被调用verify(getRequestedFor(urlEqualTo(/api/users/1)))verify(2,postRequestedFor(urlEqualTo(/api/orders)))// 验证调了2次verify(0,deleteRequestedFor(anyUrl()))// 验证没调过delete三、WireMock 的进阶玩法1. 从文件加载响应适合大JSON// 把响应体放在 src/test/resources/wiremock/stock-available.jsonstockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).willReturn(aResponse().withStatus(200).withBodyFile(wiremock/stock-available.json))// 从文件加载);2. 模拟故障场景TestDisplayName(库存服务超时订单服务应该降级)voidshouldFallbackWhenStockServiceTimeout(){// 模拟超时stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).willReturn(aResponse().withStatus(200).withFixedDelay(10000))// 10秒延迟触发超时);// 验证降级逻辑OrderResultresultorderService.createOrder(request);assertThat(result.isSuccess()).isTrue();// 降级后仍然成功assertThat(result.isStockChecked()).isFalse();// 但跳过了库存检查}TestDisplayName(库存服务返回500订单服务应该重试)voidshouldRetryWhenStockServiceError(){// 模拟500错误stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).willReturn(aResponse().withStatus(500)));// 验证重试次数assertThatThrownBy(()-orderService.createOrder(request)).isInstanceOf(StockServiceException.class);// 验证调了3次重试2次 原始1次stockServiceMock.verify(3,getRequestedFor(urlPathEqualTo(/api/stock/ITEM-001)));}3. 有状态的Mock模拟流程TestDisplayName(库存扣减后查询数量应该减少)voidshouldReflectDeductedStock(){// 初始状态有100个stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).inScenario(Stock Deduction).whenScenarioStateIs(Scenario.STARTED).willReturn(aResponse().withBody({\availableQuantity\: 100})).willSetStateTo(Deducted));// 扣减后状态剩98个stockServiceMock.stubFor(get(urlPathEqualTo(/api/stock/ITEM-001)).inScenario(Stock Deduction).whenScenarioStateIs(Deducted).willReturn(aResponse().withBody({\availableQuantity\: 98})));// 先查询StockInfobeforestockClient.queryStock(ITEM-001);assertThat(before.getAvailableQuantity()).isEqualTo(100);// 扣减stockClient.deduct(ITEM-001,2);// 再查询StockInfoafterstockClient.queryStock(ITEM-001);assertThat(after.getAvailableQuantity()).isEqualTo(98);}四、契约测试接口变更的防火墙WireMock解决了测试时模拟下游的问题但还有一个问题下游服务改了接口怎么及时发现这就是**消费者驱动契约测试Consumer-Driven Contract, CDC**要解决的。核心思想消费者订单服务 契约文件 提供者库存服务 │ │ │ │ 我期望接口长这样 │ │ │ ───────────────────── │ │ │ │ │ │ │ 验证你的实现是否符合契约 │ │ │ ─────────────────────│ │ │ │ │ │ 符合 / 不符合 │ │ │ ─────────────────────│简单说消费者定义我期望你怎么响应提供者验证我是不是按这个实现的。Spring Cloud Contract 实战1. 消费者端订单服务定义契约在订单服务的src/test/resources/contracts/下创建契约文件// shouldReturnStockInfo.groovypackagecontracts.stock org.springframework.cloud.contract.spec.Contract.make{request{methodGETurl/api/stock/ITEM-001headers{contentType(applicationJson())}}response{status200headers{contentType(applicationJson())}body([sku:ITEM-001,availableQuantity:100,reservedQuantity:5])}}2. 订单服务生成Stub并发布!-- pom.xml --plugingroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-contract-maven-plugin/artifactIdversion4.0.0/versionextensionstrue/extensionsconfigurationbaseClassForTestscom.example.BaseContractTest/baseClassForTests/configuration/plugin运行mvn clean install会自动根据契约文件生成测试代码生成Stub JAR包含WireMock映射发布到Maven仓库3. 库存服务提供者验证契约!-- 库存服务引入契约依赖 --dependencygroupIdcom.example/groupIdartifactIdorder-service/artifactIdversion1.0.0/versionclassifierstubs/classifierscopetest/scope/dependencySpringBootTest(webEnvironmentSpringBootTest.WebEnvironment.MOCK)AutoConfigureMessageVerifierclassStockServiceContractTest{AutowiredprivateStockControllerstockController;// 自动加载订单服务定义的契约验证库存服务的实现TestvoidvalidateStockContract(){// 契约测试框架自动执行}}Pact更轻量的契约测试方案如果不用Spring生态Pact是个更通用的选择。消费者端生成契约dependencygroupIdau.com.dius.pact.consumer/groupIdartifactIdjunit5/artifactIdversion4.6.0/versionscopetest/scope/dependencyExtendWith(PactConsumerTestExt.class)PactTestFor(providerNamestock-service)classStockServicePactTest{Pact(consumerorder-service)publicRequestResponsePactstockQueryPact(PactDslWithProviderbuilder){returnbuilder.given(stock exists for ITEM-001).uponReceiving(query stock for ITEM-001).path(/api/stock/ITEM-001).method(GET).willRespondWith().status(200).body(newPactDslJsonBody().stringType(sku,ITEM-001).integerType(availableQuantity,100).integerType(reservedQuantity,5)).toPact();}PactTestFor(pactMethodstockQueryPact)TestvoidshouldQueryStock(MockServermockServer){// 使用Pact生成的Mock服务测试消费者代码StockClientclientnewStockClient(mockServer.getUrl());StockInfoinfoclient.queryStock(ITEM-001);assertThat(info.getSku()).isEqualTo(ITEM-001);assertThat(info.getAvailableQuantity()).isPositive();}}运行后会在target/pacts/生成契约JSON文件{consumer:{name:order-service},provider:{name:stock-service},interactions:[{description:query stock for ITEM-001,providerState:stock exists for ITEM-001,request:{method:GET,path:/api/stock/ITEM-001},response:{status:200,body:{sku:ITEM-001,availableQuantity:100,reservedQuantity:5}}}]}提供者端验证契约Provider(stock-service)PactFolder(pacts)classStockServiceProviderVerificationTest{TestTemplateExtendWith(PactVerificationInvocationContextProvider.class)voidpactVerificationTestTemplate(PactVerificationContextcontext){context.verifyInteraction();}BeforeEachvoidbefore(PactVerificationContextcontext){context.setTarget(newHttpTestTarget(localhost,8080));}State(stock exists for ITEM-001)voidstockExistsState(){// 准备测试数据确保ITEM-001有库存stockRepository.save(newStock(ITEM-001,100,5));}}五、WireMock vs 契约测试怎么选场景用WireMock用契约测试单元/集成测试时模拟下游✅❌验证下游接口是否符合预期❌✅接口变更时自动发现不兼容❌✅跨团队协作定义接口规范辅助✅CI流水线中验证契约不行✅最佳实践两者结合开发时用WireMock快速验证提测前用契约测试确保兼容性CI流水线跑契约验证不通过不让合并六、小结今天咱们聊了微服务测试的两大武器工具解决什么问题核心能力WireMock下游服务没就绪/不稳定模拟HTTP服务控制响应内容、延迟、故障Spring Cloud ContractSpring生态的契约测试消费者定义契约提供者自动验证Pact跨语言的契约测试生成契约文件独立验证消费者和提供者一句话总结WireMock让你在孤岛上也能开发测试契约测试让团队之间的接口约定变成可执行的代码。两者结合微服务测试就稳了。你们微服务测试怎么做的用WireMock还是直接连测试环境欢迎聊聊。