Core Java

Testing gRPC Services in Java

gRPC is a high-performance RPC framework that uses protocol buffers for defining services and messages. For reliable systems, you should unit test your service logic. For gRPC services, it is common to test the service implementation with an InProcessServer (no network) and an InProcessChannel client. That lets you verify behavior end-to-end while keeping tests fast and deterministic. Let us delve into understanding how to write unit tests for a gRPC service, which are essential for verifying the correctness of gRPC service logic without relying on actual network communication. Using JUnit 5 and gRPC’s in-process server and channel, we can run fast, isolated, and deterministic tests.

1. What is gRPC?

gRPC (Google Remote Procedure Call) is a high-performance, open-source RPC framework that enables communication between distributed applications in a language-agnostic way. It is widely used for microservices architectures, cloud-native applications, and high-performance backend systems. Some of the key features of gRPC include:

  • IDL with Protocol Buffers: gRPC uses Protocol Buffers (protobuf) as the interface definition language, allowing you to define services and message types in a compact, language-neutral format.
  • Streaming: Supports client-side, server-side, and bidirectional streaming, enabling efficient real-time data transfer.
  • Pluggable authentication and interceptors: gRPC provides mechanisms for authentication, encryption, and interceptors to add custom logic like logging, metrics, or retries.
  • High performance via HTTP/2 transport: gRPC leverages HTTP/2 for multiplexed streams, header compression, and low-latency communication.

In Java, gRPC applications rely on the io.grpc packages for core functionality and the generated protobuf classes for messages and service stubs. This combination allows developers to build scalable, maintainable, and high-performance RPC-based applications.

2. Implement the gRPC client and service

2.1 Define the .proto

The following code defines the gRPC service and message structure using Protocol Buffers. Save the file as src/main/proto/greeter.proto.

syntax = "proto3";

package example.grpc;

option java_package = "com.example.grpc";
option java_outer_classname = "GreeterProto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

This .proto file defines a gRPC service named Greeter with a single remote procedure call SayHello that accepts a HelloRequest message containing a name and returns a HelloReply message containing a greeting message. The java_package and java_outer_classname options specify the generated Java package and class names for use in the gRPC server and client code.

2.2 Adding Dependencies (build.gradle)

The following Gradle configuration adds the required plugins and dependencies for building and testing a gRPC service in Java:

plugins {
  id 'java'
  id 'com.google.protobuf' version '0.9.3'
}

repositories { mavenCentral() }

dependencies {
  implementation 'io.grpc:grpc-netty-shaded:1.56.0'
  implementation 'io.grpc:grpc-protobuf:1.56.0'
  implementation 'io.grpc:grpc-stub:1.56.0'

  testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
}

protobuf {
  protoc { artifact = "com.google.protobuf:protoc:3.22.0" }
  plugins {
    grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.56.0' }
  }
  generateProtoTasks {
    all()*.plugins { grpc {} }
  }
}

test {
  useJUnitPlatform()
}

This Gradle configuration sets up the Java and Protocol Buffers plugins, includes the gRPC and JUnit dependencies, and configures the Protobuf compiler to generate both message classes and gRPC service stubs. The useJUnitPlatform() directive enables JUnit 5 for unit testing, ensuring that the gRPC project can be built and tested efficiently.

2.3 Service Implementation (Server)

The following Java class implements the gRPC server-side logic by extending the generated GreeterGrpc.GreeterImplBase class:

package com.example.grpc;

import io.grpc.stub.StreamObserver;

public class GreeterService extends GreeterGrpc.GreeterImplBase {

  @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    String name = req.getName();
    String replyMsg = "Hello, " + (name == null || name.isEmpty() ? "world" : name) + "!";

    HelloReply reply = HelloReply.newBuilder()
        .setMessage(replyMsg)
        .build();

    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }
}

This service class overrides the sayHello() method defined in the gRPC interface. It retrieves the name from the client request, constructs a greeting message, and sends it back as a HelloReply response using the StreamObserver. If no name is provided, it defaults to greeting “world.” Finally, onCompleted() signals that the response stream has finished sending data.

2.4 Server Bootstrap (Main)

The following Java class starts the gRPC server and registers the service implementation:

package com.example.grpc;

import io.grpc.Server;
import io.grpc.ServerBuilder;

public class GrpcServerMain {
  public static void main(String[] args) throws Exception {
    Server server = ServerBuilder.forPort(8080)
        .addService(new GreeterService())
        .build()
        .start();

    System.out.println("Server started on port 8080");
    server.awaitTermination();
  }
}

This code creates and launches a gRPC server on port 8080 using ServerBuilder. It registers the GreeterService to handle incoming RPC requests, starts the server, and keeps it running using awaitTermination(). The console message confirms successful startup, allowing the service to accept client connections.

2.5 Client Implementation

The following Java class implements a simple gRPC client that connects to the server and invokes the remote sayHello method:

package com.example.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class GreeterClient {
  private final GreeterGrpc.GreeterBlockingStub blockingStub;
  private final ManagedChannel channel;

  public GreeterClient(String host, int port) {
    this.channel = ManagedChannelBuilder.forAddress(host, port)
        .usePlaintext()
        .build();
    this.blockingStub = GreeterGrpc.newBlockingStub(channel);
  }

  public String sayHello(String name) {
    HelloRequest req = HelloRequest.newBuilder().setName(name).build();
    HelloReply resp = blockingStub.sayHello(req);
    return resp.getMessage();
  }

  public void shutdown() {
    channel.shutdownNow();
  }

  public static void main(String[] args) {
    GreeterClient client = new GreeterClient("localhost", 8080);
    System.out.println(client.sayHello("Alice"));
    client.shutdown();
  }
}

This client creates a communication channel to the gRPC server using ManagedChannelBuilder and initializes a GreeterBlockingStub for synchronous RPC calls. The sayHello() method sends a HelloRequest containing the user’s name and prints the server’s response. After completing the call, the channel is properly shut down to release resources.

2.6 Code Run and Output

To run the gRPC service, start the server first and then execute the client:

# Step 1: Run the server
$ ./gradlew run --args='com.example.grpc.GrpcServerMain'

# Output:
Server started on port 8080

# Step 2: In another terminal, run the client
$ ./gradlew run --args='com.example.grpc.GreeterClient'

The server should already be running on port 8080. When the client connects, it will call the sayHello method defined in the gRPC service and print the response from the server.

Server started on port 8080
Hello, Alice!

As shown in the output, the gRPC server responds to the client’s request by returning a personalized greeting message. This confirms that the client-server communication via gRPC is working correctly.

3. Unit Tests

Unit testing is essential for verifying the correctness of gRPC service logic without relying on network communication. In Java, we can use JUnit 5 along with gRPC’s in-process server and channel to run fast, isolated tests.

3.1 Test Class Implementation

The following Java class tests the GreeterService implementation using an in-process gRPC server:

package com.example.grpc;

import io.grpc.ManagedChannel;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.Server;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

public class GreeterServiceTest {

  private static Server server;
  private static ManagedChannel channel;
  private static GreeterGrpc.GreeterBlockingStub blockingStub;

  @BeforeAll
  public static void setup() throws Exception {
    String serverName = InProcessServerBuilder.generateName();

    server = InProcessServerBuilder
        .forName(serverName)
        .directExecutor()
        .addService(new GreeterService())
        .build()
        .start();

    channel = InProcessChannelBuilder
        .forName(serverName)
        .directExecutor()
        .build();

    blockingStub = GreeterGrpc.newBlockingStub(channel);
  }

  @AfterAll
  public static void tearDown() {
    channel.shutdownNow();
    server.shutdownNow();
  }

  @Test
  public void testSayHello_withName() {
    HelloRequest request = HelloRequest.newBuilder().setName("Alice").build();
    HelloReply response = blockingStub.sayHello(request);
    assertEquals("Hello, Alice!", response.getMessage());
  }

  @Test
  public void testSayHello_withoutName() {
    HelloRequest request = HelloRequest.newBuilder().setName("").build();
    HelloReply response = blockingStub.sayHello(request);
    assertEquals("Hello, world!", response.getMessage());
  }
}

This JUnit 5 test class creates an in-process gRPC server using InProcessServerBuilder and connects with an in-memory InProcessChannel client. The tests verify that sayHello() returns the correct greeting for both a provided name and an empty name, ensuring that the service behaves as expected. The server and channel are properly shut down after all tests to release resources.

3.2 Test Execution & Output

To run the unit tests for the GreeterService, use the following Gradle command:

# Run JUnit tests
$ ./gradlew test

After execution, Gradle will compile and run all test classes. The output should indicate that all tests passed successfully:

> Task :test

GreeterServiceTest > testSayHello_withName PASSED
GreeterServiceTest > testSayHello_withoutName PASSED

BUILD SUCCESSFUL in 3s
5 actionable tasks: 5 executed

This output confirms that the GreeterService behaves as expected, handling both named and default greetings correctly. Using in-process servers ensures tests are fast, isolated, and deterministic, making it easy to catch regressions during development.

4. Conclusion

gRPC is a high-performance, language-agnostic RPC framework that leverages Protocol Buffers to define services and messages in a compact and efficient way. It provides a powerful mechanism for building scalable and fast client-server communication over HTTP/2.

For building reliable systems, it is essential to unit test your service logic. In gRPC-based applications, the recommended approach is to test the service implementation using an InProcessServer (which avoids real network communication) together with an InProcessChannel client. This method enables true end-to-end verification of service behavior while keeping tests fast, isolated, and deterministic.

By following this approach, you ensure that your gRPC services are not only performant in production but also maintainable and robust throughout development and testing.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button