GRPC
GRPC 是一种建立在 HTTP/2 之上的 RPC 框架,Spring Cloud Contract 对其提供基本支持。
Spring Cloud Contract 对 gRPC 的基本用例有实验性的支持。不幸的是,由于 gRPC 调整了 HTTP/2 标头帧,因此无法声明 grpc-status
标头。
我们来看一下以下合同。
Groovy contract
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaData
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/beer.BeerService/check'
body(fileAsBytes("PersonToCheck_old_enough.bin"))
headers {
contentType("application/grpc")
header("te", "trailers")
}
}
response {
status 200
body(fileAsBytes("Response_old_enough.bin"))
headers {
contentType("application/grpc")
header("grpc-encoding", "identity")
header("grpc-accept-encoding", "gzip")
}
}
metadata([
"verifierHttp": [
"protocol": ContractVerifierHttpMetaData.Protocol.H2_PRIOR_KNOWLEDGE.toString()
]
])
}
Producer Side Setup
为了利用 HTTP/2 支持,您必须设置 CUSTOM
测试模式,如下所示。
Maven
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>CUSTOM</testMode>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
</configuration>
</plugin>
Gradle
contracts {
packageWithBaseClasses = 'com.example'
testMode = "CUSTOM"
}
基类将设置在随机端口上运行的应用程序。它还将 HttpVerifier
实现设置为可以使用 HTTP/2 协议的实现。Spring Cloud Contract 附带 OkHttpHttpVerifier
实现。
Base Class
@SpringBootTest(classes = BeerRestBase.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"grpc.server.port=0"
})
public abstract class BeerRestBase {
@Autowired
GrpcServerProperties properties;
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
ProducerController producerController(PersonCheckingService personCheckingService) {
return new ProducerController(personCheckingService);
}
@Bean
PersonCheckingService testPersonCheckingService() {
return argument -> argument.getAge() >= 20;
}
@Bean
HttpVerifier httpOkVerifier(GrpcServerProperties properties) {
return new OkHttpHttpVerifier("localhost:" + properties.getPort());
}
}
}
Consumer Side Setup
GRPC 消费者端测试示例。由于 GRPC 服务器端的异常行为,存根无法在适当的时候返回 grpc-status
标头。这就是我们需要手动设置返回状态的原因。
Consumer Side Test
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = GrpcTests.TestConfiguration.class, properties = {
"grpc.client.beerService.address=static://localhost:5432", "grpc.client.beerService.negotiationType=TLS"
})
public class GrpcTests {
@GrpcClient(value = "beerService", interceptorNames = "fixedStatusSendingClientInterceptor")
BeerServiceGrpc.BeerServiceBlockingStub beerServiceBlockingStub;
int port;
@RegisterExtension
static StubRunnerExtension rule = new StubRunnerExtension()
.downloadStub("com.example", "beer-api-producer-grpc")
// With WireMock PlainText mode you can just set an HTTP port
// .withPort(5432)
.stubsMode(StubRunnerProperties.StubsMode.LOCAL)
.withHttpServerStubConfigurer(MyWireMockConfigurer.class);
@BeforeEach
public void setupPort() {
this.port = rule.findStubUrl("beer-api-producer-grpc").getPort();
}
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(23).build());
BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.OK);
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(17).build());
response = response == null ? Response.newBuilder().build() : response;
BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.NOT_OK);
}
// Not necessary with WireMock PlainText mode
static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer {
@Override
public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
return httpStubConfiguration
.httpsPort(5432);
}
}
@Configuration
@ImportAutoConfiguration(GrpcClientAutoConfiguration.class)
static class TestConfiguration {
// Not necessary with WireMock PlainText mode
@Bean
public GrpcChannelConfigurer keepAliveClientConfigurer() {
return (channelBuilder, name) -> {
if (channelBuilder instanceof NettyChannelBuilder) {
try {
((NettyChannelBuilder) channelBuilder)
.sslContext(GrpcSslContexts.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build());
}
catch (SSLException e) {
throw new IllegalStateException(e);
}
}
};
}
/**
* GRPC client interceptor that sets the returned status always to OK.
* You might want to change the return status depending on the received stub payload.
*
* Hopefully in the future this will be unnecessary and will be removed.
*/
@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) {
// TODO: This must be fixed somehow either in Jetty (WireMock) or somewhere else
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);
}
};
}
};
}
}
}