Spring Cloud Contract gRPC测试:WireMock请求匹配失败排查
Spring Cloud Contract gRPC契约测试请求匹配问题排查与解决
问题描述
基于Spring Cloud Contract构建gRPC+Protocol Buffers服务的集成契约测试套件,生产者端Stub已生成并可通过stub-runner正常使用,但消费者端调用Stub时,WireMock抛出请求不匹配错误,尽管使用了与契约定义完全一致的Protobuf请求消息。
错误日志
2022-08-23 08:40:23.211 ERROR [WireMock] Request was not matched ======================= --------------------------------------------------------------------------------------------- | Closest stub | Request | --------------------------------------------------------------------------------------------- | POST | POST /service/method | /service/method | Content-Type [matches] : application/grpc.* | Content-Type: application/grpc te: trailers | te: trailers | AAAAGz0AAAAbOAAAABszEqYhWwogIHsKICAgICJAdHlwZSI6ICJRRUZpb | [B@1a1a8bcc HRlclBpcGUiLAogICAgImZpbHRlckV4cHIiOiB7CiAgICAgICJAdHlwZS | <<<<<< Body does not match I6ICJFeHByRnVuY3Rpb24iLAogICAgICAiZnVuY3Rpb24iOiB7CiAgICA | ...more base64 code... |
注:Stub侧请求体以Base64格式展示,消费者侧仅显示字节数组默认toString结果,难以直接排查差异。
消费者测试代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = ConsumerTest.TestConfiguration.class, properties = { "grpc.client.myGrpcService.address=static://localhost:5433", "grpc.client.myGrpcService.negotiationType=TLS" }) public class ConsumerTest { @GrpcClient(value = "myGrpcService", interceptorNames = "fixedStatusSendingClientInterceptor") MyGrpcServiceGrpc.MyGrpcServiceBlockingStub blockingStub; int port; @RegisterExtension static StubRunnerExtension rule = new StubRunnerExtension() .downloadStub("mygroupid", "myartifactid", "0.0.0", "stubs") .stubsMode(StubRunnerProperties.StubsMode.LOCAL) .withHttpServerStubConfigurer(MyWireMockConfigurer.class); @BeforeEach public void setupPort() { this.port = rule.findStubUrl("mygroupid", "myartifactid").getPort(); } @Test void initialTest() { MyProtoMessage request = MyProtoMessage.newBuilder() [...] .build(); Iterator<MyResponse> iterator = blockingStub.myMethod(request); // no element found, since the request does not match MyResponse current = iterator.next(); } static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer { @Override public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) { return httpStubConfiguration.dynamicHttpsPort() .httpsPort(5433); } } @Configuration @ImportAutoConfiguration(GrpcClientAutoConfiguration.class) static class TestConfiguration { @Bean public GrpcChannelConfigurer keepAliveClientConfigurer() { return (channelBuilder, name) -> { if (channelBuilder instanceof NettyChannelBuilder) { try { ((NettyChannelBuilder) channelBuilder) .useTransportSecurity() .negotiationType(NegotiationType.TLS) .sslContext(GrpcSslContexts.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build()); } catch (SSLException e) { throw new IllegalStateException(e); } } }; } @Bean ClientInterceptor fixedStatusSendingClientInterceptor() { return new ClientInterceptor() { @Override public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { ClientCall<ReqT, RespT> call = next.newCall(method, callOptions); return new ClientCall<ReqT, RespT>() { @Override public void start(Listener<RespT> responseListener, Metadata headers) { Listener<RespT> listener = new Listener<RespT>() { @Override public void onHeaders(Metadata headers) { responseListener.onHeaders(headers); } @Override public void onMessage(RespT message) { responseListener.onMessage(message); } @Override public void onClose(Status status, Metadata trailers) { responseListener.onClose(Status.OK, trailers); } @Override public void onReady() { responseListener.onReady(); } }; call.start(listener, headers); } @Override public void request(int numMessages) { call.request(numMessages); } @Override public void cancel(@Nullable String message, @Nullable Throwable cause) { call.cancel(message, cause); } @Override public void halfClose() { call.halfClose(); } @Override public void sendMessage(ReqT message) { call.sendMessage(message); } }; } }; } } }
契约定义
package contracts import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaData Contract.make { description("Contract Test") request { method 'POST' url '/service/method' body(fileAsBytes("request.bin")) headers { contentType("application/grpc") header("te", "trailers") } } response { status 200 body(value(producer($(anyNonEmptyString())), consumer(fileAsBytes("response.bin")))) headers { contentType("application/grpc") header("grpc-encoding", "identity") header("grpc-accept-encoding", "gzip") } } metadata([ "verifierHttp": [ "protocol": ContractVerifierHttpMetaData.Protocol.H2_PRIOR_KNOWLEDGE.toString() ] ]) }
解决方案
1. 开启WireMock的HTTP/2支持
gRPC基于HTTP/2协议运行,需确保WireMock启用HTTP/2:
static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer { @Override public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) { return httpStubConfiguration.dynamicHttpsPort() .http2Enabled(true); // 新增启用HTTP/2 } }
2. 校验请求二进制数据一致性
对比消费者发送的Protobuf序列化字节与契约中request.bin的内容:
@Test void verifyRequestBytes() throws IOException { MyProtoMessage request = MyProtoMessage.newBuilder() [...] // 填充与契约一致的字段 .build(); byte[] requestBytes = request.toByteArray(); // 读取契约中的request.bin文件路径需根据实际项目调整 byte[] expectedBytes = Files.readAllBytes(Paths.get("src/test/resources/contracts/request.bin")); assertArrayEquals(expectedBytes, requestBytes); }
若存在差异,检查字段默认值、枚举值、嵌套结构是否与契约定义完全匹配。
3. 修正端口配置冲突
移除固定端口配置,动态获取Stub Runner分配的端口:
- 移除
MyWireMockConfigurer中的.httpsPort(5433) - 修改
@SpringBootTest属性,移除固定端口配置 - 在
setupPort中动态设置gRPC客户端地址:
@BeforeEach public void setupPort() { this.port = rule.findStubUrl("mygroupid", "myartifactid").getPort(); System.setProperty("grpc.client.myGrpcService.address", "static://localhost:" + this.port); }
4. 排查自定义拦截器影响
暂时移除fixedStatusSendingClientInterceptor,测试请求是否匹配:
- 注释
@GrpcClient中的interceptorNames参数 - 删除
TestConfiguration中的拦截器Bean定义
若请求匹配成功,再排查拦截器是否修改了请求内容或元数据。
5. 启用WireMock DEBUG日志
添加日志配置查看完整请求体:
logging.level.com.github.tomakehurst.wiremock=DEBUG
日志会输出消费者发送的完整二进制请求体,可与契约中的request.bin解码后对比。
内容的提问来源于stack exchange,提问作者Felix
相关产品推荐
相关产品推荐

