测试服务
本节介绍如何为您的 grpc-service 编写测试用例。
如果你想要测试一个使用了 @GrpcClient
注解字段或一个 grpc 的 stub。 请参阅 测试Grpc-Stubs。
目录
附加主题
前言
我们都知道测试对我们的应用程序是多么重要,所以我只会在这里向大家介绍几个链接:
通常有三种方法来测试您的 grpc 服务:
- 直接测试
- 通过 grpc 测试
- 在生产环境中测试它们 (构建时的自动化测试除外)
测试服务
让我们假设,我们希望测试以下服务:
@GrpcService
public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase {
private OtherDependency foobar;
@Autowired
public void setFoobar(OtherDependency foobar) {
this.foobar = foobar;
}
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
HelloReply response = HelloReply.newBuilder()
.setMessage("Hello ==> " + request.getName())
.setCounter(foobar.getCount())
.build();
responseObserver.onNext(response);
responseObserver.onComplete();
}
}
有用的依赖项
在您开始编写自己的测试框架之前,您可能想要使用以下库来使您的工作更加简单。
注意: Spring-Boot-Test已经包含一些依赖项,所以请确保您排除掉了冲突的版本。
对于Maven来说,添加以下依赖:
<!-- JUnit-Test-Framework -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- Grpc-Test-Support -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring-Test-Support (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Gradle 使用:
// JUnit-Test-Framework
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
// Grpc-Test-Support
testImplementation("io.grpc:grpc-testing")
// Spring-Test-Support (Optional)
testImplementation("org.springframework.boot:spring-boot-starter-test")
单元测试
在直接测试中,我们直接在 grpc-service bean/实例上调用方法。
如果您自己创建的 grpc-service 实例, 请确保您先处理所需的依赖关系。 如果您使用Spring,它会处理您的依赖关系,但作为代价,您必须配置Spring。
独立测试
独立测试对外部库没有任何依赖关系(事实上你甚至不需要这个项目)。 然而,没有外部依赖关系并不总能使您的生活更加容易, 您可能需要复制其他库来执行您的行为。 使用 Mockito 这样的模拟库会简化你的流程,因为它限制依赖树的深度。
public class MyServiceTest {
private MyServiceImpl myService;
@BeforeEach
public void setup() {
myService = new MyServiceImpl();
OtherDependency foobar = ...; // mock(OtherDependency.class)
myService.setFoobar(foobar);
}
@Test
void testSayHellpo() throws Exception {
HelloRequest request = HelloRequest.newBuilder()
.setName("Test")
.build();
StreamRecorder<HelloReply> responseObserver = StreamRecorder.create();
myService.sayHello(request, responseObserver);
if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) {
fail("The call did not terminate in time");
}
assertNull(responseObserver.getError());
List<HelloReply> results = responseObserver.getValues();
assertEquals(1, results.size());
HelloReply response = results.get(0);
assertEquals(HelloReply.newBuilder()
.setMessage("Hello ==> Test")
.setCounter(1337)
.build(), response);
}
}
基于Spring的测试
如果您使用Spring来管理您自己的依赖关系,您实际上正在进入集成测试领域。 请确保您不要启动整个应用程序,而只提供所需的依赖关系为 (模拟) 的 Bean 类。
注意: 在测试期间,Spring 不会自动配置所有必须的 Bean。 您必须在有
@Configuration
注解的类中手动创建它们。
@SpringBootTest
@SpringJUnitConfig(classes = { MyServiceUnitTestConfiguration.class })
// Spring doesn't start without a config (might be empty)
// Don't use @EnableAutoConfiguration in this scenario
public class MyServiceTest {
@Autowired
private MyServiceImpl myService;
@Test
void testSayHellpo() throws Exception {
HelloRequest request = HelloRequest.newBuilder()
.setName("Test")
.build();
StreamRecorder<HelloReply> responseObserver = StreamRecorder.create();
myService.sayHello(request, responseObserver);
if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) {
fail("The call did not terminate in time");
}
assertNull(responseObserver.getError());
List<HelloReply> results = responseObserver.getValues();
assertEquals(1, results.size());
HelloReply response = results.get(0);
assertEquals(HelloReply.newBuilder()
.setMessage("Hello ==> Test")
.setCounter(1337)
.build(), response);
}
}
和所需的配置类:
@Configuration
public class MyServiceUnitTestConfiguration {
@Bean
OtherDependency foobar() {
// return mock(OtherDependency.class);
}
@Bean
MyServiceImpl myService() {
return new MyServiceImpl();
}
}
集成测试
然而,您有时需要测试整个调用栈。 例如,如果认证发挥了作用。 但在这种情况下,建议限制您的测试范围,以避免像 空数据库这样可能的外部影响。
在这一点上,不使用 Spring 测试您的 Spring 应用程序是毫无意义的。
注意: 在测试期间,Spring 不会自动配置所有必须的 Bean。 您必须在有
@Configuration
注解修饰的类中手动创建他们,或显式的包含相关的自动配置类。
@SpringBootTest(properties = {
"grpc.server.inProcessName=test", // Enable inProcess server
"grpc.server.port=-1", // Disable external server
"grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server
})
@SpringJUnitConfig(classes = { MyServiceIntegrationTestConfiguration.class })
// Spring doesn't start without a config (might be empty)
@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test
// Avoids "port already in use" during tests
public class MyServiceTest {
@GrpcClient("inProcess")
private MyServiceBlockingStub myService;
@Test
@DirtiesContext
public void testSayHello() {
HelloRequest request = HelloRequest.newBuilder()
.setName("test")
.build();
HelloReply response = myService.sayHello(request);
assertNotNull(response);
assertEquals("Hello ==> Test", response.getMessage())
}
}
所需的配置看起来像这样:
@Configuration
@ImportAutoConfiguration({
GrpcServerAutoConfiguration.class, // Create required server beans
GrpcServerFactoryAutoConfiguration.class, // Select server implementation
GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation
public class MyServiceIntegrationTestConfiguration {
@Bean
OtherDependency foobar() {
return ...; // mock(OtherDependency.class);
}
@Bean
MyServiceImpl myServiceImpl() {
return new MyServiceImpl();
}
}
注意:这个代码看起来可能比单元测试更短/更简单,但执行时间要长一些。
gRPCurl
gRPCurl
是一个小型的命令行应用程序, 您可以在应用程序运行时查询。 或者如它们的 Readme 说的那样:
对于 gRPC 服务,它基本上是
curl
工具
您甚至可以使用 jq
工具来处理的响应,将它用于自动化测试中。
如果您已经知道要查询什么,跳过第一/这个部分。
$ # First scan the server for available services
$ grpcurl --plaintext localhost:9090 list
net.devh.boot.grpc.example.MyService
$ # Then list the methods available for that call
$ grpcurl --plaintext localhost:9090 list net.devh.boot.grpc.example.MyService
net.devh.boot.grpc.example.MyService.SayHello
$ # Lets check the request and response types
$ grpcurl --plaintext localhost:9090 describe net.devh.boot.grpc.example.MyService/SayHello
net.devh.boot.grpc.example.MyService.SayHello is a method:
rpc SayHello ( .HelloRequest ) returns ( .HelloReply );
$ # Now we only have query for the request body structure
$ grpcurl --plaintext localhost:9090 describe net.devh.boot.grpc.example.HelloRequest
net.devh.boot.grpc.example.HelloRequest is a message:
message HelloRequest {
string name = 1;
}
注意:
gRPCurl
支持.
和/
作为服务名称和方法名称之间的分隔符:
net.devh.boot.grpc.example.MyService.SayHello
net.devh.boot.grpc.example.MyService/SayHello
我们推荐第二种方式,因为它跟 grpc 的内部全方法名称匹配,并且方法名称在调用中更容易识别到。
$ # Finally we can call the actual method
$ grpcurl --plaintext localhost:9090 net.devh.boot.grpc.example.MyService/SayHello
{
"message": "Hello ==> ",
"counter": 1337
}
$ # Or call it with a populated request body
$ grpcurl --plaintext -d '{"name": "Test"}' localhost:9090 net.devh.boot.grpc.example.MyService/SayHello
{
"message": "Hello ==> Test",
"counter": 1337
}
注意:如果您使用了 window 终端或想要在数据块中使用变量,那么您必须使用
"
而不是'
,并转义实际json中的"
。````cmd
grpcurl –plaintext -d “{"name": "Test"}” localhost:9090 net.devh.boot.grpc.example.MyService/sayHello { “message”: “Hello ==> Test”, “counter”: 1337 } ````
更多 gRPCurl
的信息,请参阅他们的 官方文档