Testing the Service

<- Back to Index

This section describes how you write tests for your grpc-service implementation.

If you want to test a component that internally uses an @GrpcClient annotated field or one of grpc’s stubs. Please refer to Tests with Grpc-Stubs.

Table of Contents

Additional Topics

Introductory Words

We all know how important it is to test our application, so I will only refer you to a few links here:

Generally there are three ways to test your grpc service:

The Service to Test

Let’s assume that we wish to test the following service:

@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();
    }

}

Useful Dependencies

Before you start writing your own test framework, you might want to use the following libraries to make your work easier.

Note: Spring-Boot-Test already contains some of these dependencies, so make sure you exclude conflicting versions.

For Maven add the following dependencies:

<!-- 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>
    <!-- Exclude the test engine you don't need -->
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

For Gradle use:

// 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") {
    // Exclude the test engine you don't need
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}

Unit Tests

In the direct tests, we invoke the methods directly on the grpc-service bean/instance.

If you create the grpc-service instance yourself, make sure that you populate the required dependencies first. If you use Spring, it will take care of the dependencies for you, but in return you will have to configure Spring.

Standalone Tests

The standalone tests don’t have any dependencies to external libraries (in fact you don’t even need this one). However, having no external dependencies doesn’t always make your life easier, because you might have to replicate behavior that other libraries might do for you. Using a mocking library such as Mockito will simplify the process for you, as it limits the depth of the dependency tree.

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-based Tests

If you use Spring to manage dependencies for yourself, you’re actually tapping into the field of integration tests. Make sure you don’t start the entire application, but only provide the required dependencies as (mocked) beans.

Note: During tests spring does not automatically setup all required beans. You have to manually create them in your @Configuration classes.

@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);
    }

}

and the required configuration class:

@Configuration
public class MyServiceUnitTestConfiguration {

    @Bean
    OtherDependency foobar() {
        // return mock(OtherDependency.class);
    }

    @Bean
    MyServiceImpl myService() {
        return new MyServiceImpl();
    }

}

Integration Tests

Sometimes, however, you need to test the entire stack. For example, if authentication plays a role. But also in this case it is recommended to limit the scope of your test to avoid possible external influences like an empty database.

At this point it doesn’t make any sense to test your spring based application without spring.

Note: During tests spring does not automatically setup all required beans. You have to manually create them in your @Configuration or explicitly include the related auto configuration classes.

@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())
    }

}

and the required configuration looks like this:

@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();
    }

}

Note: This code might look shorter/simpler than the unit test one, but the execution time is serveral times longer.

gRPCurl

gRPCurl is a small command line application, that you can use to query your application at runtime. Or as their Readme states:

It’s basically curl for gRPC servers.

You can even use the responses with jq and use it in your automation.

Skip the first/this block if you already know what you wish to query.

$ # 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;
}

Note: gRPCurl supports both . and / as separator between the service name and the method name:

We recommend the second variant as it matches grpc’s internal full method name and the method name is easier to detect in the call.

$ # 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
}

Note: If you use the windows terminal or wish to use variables inside the data block then you have to use " instead of ' and escape the " that are part of the actual json.

> grpcurl --plaintext -d "{\"name\": \"Test\"}" localhost:9090 net.devh.boot.grpc.example.MyService/sayHello
{
  "message": "Hello ==> Test",
  "counter": 1337
}

For more information regarding gRPCurl please refer to their official documentation

Additional Topics


<- Back to Index