gRPC testing requires specialized approaches due to its binary protocol and streaming capabilities. This guide covers comprehensive testing strategies for gRPC services in Java, including unit testing, integration testing, and advanced scenarios.
Project Setup and Dependencies
1. Maven Dependencies
<dependencies> <!-- gRPC Core --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-core</artifactId> <version>1.59.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.59.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.59.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty</artifactId> <version>1.59.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-services</artifactId> <version>1.59.0</version> </dependency> <!-- Testing Dependencies --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-testing</artifactId> <version>1.59.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <version>4.2.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.6.1</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.25.1:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.59.0:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build> 2. Sample Protocol Buffer Definition
// user_service.proto syntax = "proto3"; package com.example.grpc; option java_package = "com.example.grpc"; option java_multiple_files = true; service UserService { rpc GetUser (GetUserRequest) returns (UserResponse); rpc CreateUser (CreateUserRequest) returns (UserResponse); rpc ListUsers (ListUsersRequest) returns (stream UserResponse); rpc UpdateUsers (stream UpdateUserRequest) returns (UpdateUserSummary); rpc Chat (stream ChatMessage) returns (stream ChatMessage); } message GetUserRequest { string user_id = 1; } message CreateUserRequest { string name = 1; string email = 2; int32 age = 3; } message ListUsersRequest { int32 page_size = 1; string page_token = 2; } message UpdateUserRequest { string user_id = 1; string name = 2; string email = 3; } message UpdateUserSummary { int32 updated_count = 1; } message UserResponse { string user_id = 1; string name = 2; string email = 3; int32 age = 4; string created_at = 5; } message ChatMessage { string user_id = 1; string message = 2; string timestamp = 3; } Unit Testing gRPC Services
1. Testing Unary RPC Calls
// UserServiceTest.java import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.testing.GrpcCleanupRule; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; private UserServiceGrpc.UserServiceBlockingStub blockingStub; private UserServiceGrpc.UserServiceStub asyncStub; private grpcCleanup grpcCleanup = new GrpcCleanupRule(); @BeforeEach void setUp() throws Exception { // Generate a unique in-process server name String serverName = InProcessServerBuilder.generateName(); // Create a server, add service, start, and register for automatic shutdown grpcCleanup.register(InProcessServerBuilder .forName(serverName) .directExecutor() .addService(new UserServiceImpl(userRepository)) .build() .start()); // Create a client channel and register for automatic shutdown blockingStub = UserServiceGrpc.newBlockingStub( grpcCleanup.register(InProcessChannelBuilder .forName(serverName) .directExecutor() .build())); asyncStub = UserServiceGrpc.newStub( grpcCleanup.register(InProcessChannelBuilder .forName(serverName) .directExecutor() .build())); } @Test void testGetUser_Success() { // Given String userId = "user-123"; User user = new User(userId, "John Doe", "[email protected]", 30); when(userRepository.findById(userId)).thenReturn(user); // When GetUserRequest request = GetUserRequest.newBuilder() .setUserId(userId) .build(); UserResponse response = blockingStub.getUser(request); // Then assertNotNull(response); assertEquals(userId, response.getUserId()); assertEquals("John Doe", response.getName()); assertEquals("[email protected]", response.getEmail()); verify(userRepository).findById(userId); } @Test void testGetUser_NotFound() { // Given String userId = "non-existent"; when(userRepository.findById(userId)).thenReturn(null); // When & Then GetUserRequest request = GetUserRequest.newBuilder() .setUserId(userId) .build(); assertThrows(io.grpc.StatusRuntimeException.class, () -> { blockingStub.getUser(request); }); verify(userRepository).findById(userId); } @Test void testCreateUser_Success() { // Given User user = new User("user-456", "Jane Smith", "[email protected]", 25); when(userRepository.save(any(User.class))).thenReturn(user); // When CreateUserRequest request = CreateUserRequest.newBuilder() .setName("Jane Smith") .setEmail("[email protected]") .setAge(25) .build(); UserResponse response = blockingStub.createUser(request); // Then assertNotNull(response); assertEquals("user-456", response.getUserId()); assertEquals("Jane Smith", response.getName()); assertEquals("[email protected]", response.getEmail()); verify(userRepository).save(any(User.class)); } } 2. Testing Server Streaming RPC
// ServerStreamingTest.java import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ServerStreamingTest { @Mock private UserRepository userRepository; @Captor private ArgumentCaptor<StreamObserver<UserResponse>> responseObserverCaptor; @Test void testListUsers_ServerStreaming() throws Exception { // Given UserServiceImpl service = new UserServiceImpl(userRepository); List<User> users = List.of( new User("user-1", "User One", "[email protected]", 25), new User("user-2", "User Two", "[email protected]", 30), new User("user-3", "User Three", "[email protected]", 35) ); when(userRepository.findAll(anyInt(), anyString())).thenReturn(users); // Mock response observer @SuppressWarnings("unchecked") StreamObserver<UserResponse> responseObserver = mock(StreamObserver.class); // When ListUsersRequest request = ListUsersRequest.newBuilder() .setPageSize(10) .build(); service.listUsers(request, responseObserver); // Then verify(responseObserver, times(3)).onNext(any(UserResponse.class)); verify(responseObserver).onCompleted(); verify(responseObserver, never()).onError(any(Throwable.class)); // Capture individual responses ArgumentCaptor<UserResponse> responseCaptor = ArgumentCaptor.forClass(UserResponse.class); verify(responseObserver, times(3)).onNext(responseCaptor.capture()); List<UserResponse> capturedResponses = responseCaptor.getAllValues(); assertEquals(3, capturedResponses.size()); assertEquals("User One", capturedResponses.get(0).getName()); } @Test void testListUsers_WithRealObserver() throws Exception { // Given UserServiceImpl service = new UserServiceImpl(userRepository); List<User> users = List.of( new User("user-1", "User One", "[email protected]", 25), new User("user-2", "User Two", "[email protected]", 30) ); when(userRepository.findAll(anyInt(), anyString())).thenReturn(users); // Use real observer with latch for async testing CountDownLatch latch = new CountDownLatch(1); List<UserResponse> receivedResponses = new ArrayList<>(); StreamObserver<UserResponse> responseObserver = new StreamObserver<>() { @Override public void onNext(UserResponse value) { receivedResponses.add(value); } @Override public void onError(Throwable t) { latch.countDown(); } @Override public void onCompleted() { latch.countDown(); } }; // When ListUsersRequest request = ListUsersRequest.newBuilder() .setPageSize(10) .build(); service.listUsers(request, responseObserver); // Wait for completion assertTrue(latch.await(5, TimeUnit.SECONDS)); // Then assertEquals(2, receivedResponses.size()); assertEquals("User One", receivedResponses.get(0).getName()); assertEquals("User Two", receivedResponses.get(1).getName()); } } 3. Testing Client Streaming RPC
// ClientStreamingTest.java import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ClientStreamingTest { @Mock private UserRepository userRepository; @Captor private ArgumentCaptor<StreamObserver<UpdateUserSummary>> responseObserverCaptor; @Test void testUpdateUsers_ClientStreaming() throws Exception { // Given UserServiceImpl service = new UserServiceImpl(userRepository); when(userRepository.update(any(User.class))).thenReturn(true); // Capture the response observer @SuppressWarnings("unchecked") StreamObserver<UpdateUserSummary> responseObserver = mock(StreamObserver.class); // When - get the request observer StreamObserver<UpdateUserRequest> requestObserver = service.updateUsers(responseObserver); // Send multiple requests requestObserver.onNext(UpdateUserRequest.newBuilder() .setUserId("user-1") .setName("Updated Name 1") .setEmail("[email protected]") .build()); requestObserver.onNext(UpdateUserRequest.newBuilder() .setUserId("user-2") .setName("Updated Name 2") .setEmail("[email protected]") .build()); requestObserver.onCompleted(); // Then verify(userRepository, times(2)).update(any(User.class)); // Verify the summary response ArgumentCaptor<UpdateUserSummary> summaryCaptor = ArgumentCaptor.forClass(UpdateUserSummary.class); verify(responseObserver).onNext(summaryCaptor.capture()); verify(responseObserver).onCompleted(); UpdateUserSummary summary = summaryCaptor.getValue(); assertEquals(2, summary.getUpdatedCount()); } @Test void testUpdateUsers_ClientStreamingWithError() { // Given UserServiceImpl service = new UserServiceImpl(userRepository); when(userRepository.update(any(User.class))).thenThrow(new RuntimeException("Database error")); @SuppressWarnings("unchecked") StreamObserver<UpdateUserSummary> responseObserver = mock(StreamObserver.class); // When StreamObserver<UpdateUserRequest> requestObserver = service.updateUsers(responseObserver); requestObserver.onNext(UpdateUserRequest.newBuilder() .setUserId("user-1") .setName("Test User") .build()); // Then verify(responseObserver).onError(any(Throwable.class)); verify(responseObserver, never()).onNext(any()); verify(responseObserver, never()).onCompleted(); } } 4. Testing Bidirectional Streaming RPC
// BidirectionalStreamingTest.java import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class BidirectionalStreamingTest { @Mock private ChatMessageProcessor messageProcessor; @Test void testChat_BidirectionalStreaming() throws Exception { // Given ChatServiceImpl service = new ChatServiceImpl(messageProcessor); when(messageProcessor.process(any(ChatMessage.class))) .thenAnswer(invocation -> { ChatMessage request = invocation.getArgument(0); return ChatMessage.newBuilder() .setUserId("server") .setMessage("Echo: " + request.getMessage()) .setTimestamp(String.valueOf(System.currentTimeMillis())) .build(); }); CountDownLatch latch = new CountDownLatch(1); List<ChatMessage> receivedMessages = new ArrayList<>(); @SuppressWarnings("unchecked") StreamObserver<ChatMessage> responseObserver = new StreamObserver<>() { @Override public void onNext(ChatMessage value) { receivedMessages.add(value); } @Override public void onError(Throwable t) { latch.countDown(); } @Override public void onCompleted() { latch.countDown(); } }; // When - get the request observer StreamObserver<ChatMessage> requestObserver = service.chat(responseObserver); // Send multiple messages requestObserver.onNext(ChatMessage.newBuilder() .setUserId("client-1") .setMessage("Hello") .setTimestamp("12345") .build()); requestObserver.onNext(ChatMessage.newBuilder() .setUserId("client-1") .setMessage("How are you?") .setTimestamp("12346") .build()); // Wait a bit for processing Thread.sleep(100); // Complete the client stream requestObserver.onCompleted(); // Then assertTrue(latch.await(2, TimeUnit.SECONDS)); assertEquals(2, receivedMessages.size()); assertTrue(receivedMessages.get(0).getMessage().contains("Echo: Hello")); verify(messageProcessor, times(2)).process(any(ChatMessage.class)); } } Integration Testing with In-Process Server
1. Comprehensive Integration Test
// UserServiceIntegrationTest.java import io.grpc.*; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceIntegrationTest { @Mock private UserRepository userRepository; private Server server; private ManagedChannel channel; private UserServiceGrpc.UserServiceBlockingStub blockingStub; private UserServiceGrpc.UserServiceStub asyncStub; private final String serverName = InProcessServerBuilder.generateName(); @BeforeEach void setUp() throws IOException { // Create and start server server = InProcessServerBuilder .forName(serverName) .directExecutor() .addService(new UserServiceImpl(userRepository)) .build() .start(); // Create client channel channel = InProcessChannelBuilder .forName(serverName) .directExecutor() .build(); blockingStub = UserServiceGrpc.newBlockingStub(channel); asyncStub = UserServiceGrpc.newStub(channel); } @AfterEach void tearDown() throws InterruptedException { channel.shutdown(); server.shutdown(); channel.awaitTermination(5, TimeUnit.SECONDS); server.awaitTermination(5, TimeUnit.SECONDS); } @Test void testCompleteUserWorkflow() { // Test Create User User createdUser = new User("user-123", "Test User", "[email protected]", 30); when(userRepository.save(any(User.class))).thenReturn(createdUser); CreateUserRequest createRequest = CreateUserRequest.newBuilder() .setName("Test User") .setEmail("[email protected]") .setAge(30) .build(); UserResponse createResponse = blockingStub.createUser(createRequest); assertEquals("user-123", createResponse.getUserId()); assertEquals("Test User", createResponse.getName()); // Test Get User when(userRepository.findById("user-123")).thenReturn(createdUser); GetUserRequest getRequest = GetUserRequest.newBuilder() .setUserId("user-123") .build(); UserResponse getResponse = blockingStub.getUser(getRequest); assertEquals("user-123", getResponse.getUserId()); assertEquals("[email protected]", getResponse.getEmail()); verify(userRepository).save(any(User.class)); verify(userRepository).findById("user-123"); } @Test void testErrorPropagation() { // Given when(userRepository.findById("invalid-user")) .thenThrow(new RuntimeException("Database connection failed")); // When & Then GetUserRequest request = GetUserRequest.newBuilder() .setUserId("invalid-user") .build(); StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> { blockingStub.getUser(request); }); assertEquals(Status.INTERNAL.getCode(), exception.getStatus().getCode()); assertTrue(exception.getMessage().contains("Database connection failed")); } } Testing gRPC Interceptors
1. Interceptor Implementation
// AuthenticationInterceptor.java import io.grpc.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AuthenticationInterceptor implements ServerInterceptor { private static final Logger logger = LoggerFactory.getLogger(AuthenticationInterceptor.class); private static final Metadata.Key<String> AUTH_HEADER = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { String authHeader = headers.get(AUTH_HEADER); if (authHeader == null || !authHeader.startsWith("Bearer ")) { call.close(Status.UNAUTHENTICATED.withDescription("Missing or invalid authorization header"), new Metadata()); return new ServerCall.Listener<ReqT>() {}; } String token = authHeader.substring(7); if (!isValidToken(token)) { call.close(Status.UNAUTHENTICATED.withDescription("Invalid token"), new Metadata()); return new ServerCall.Listener<ReqT>() {}; } // Set user context for the call Context context = Context.current().withValue(UserContext.USER_ID_CTX, extractUserId(token)); return Contexts.interceptCall(context, call, headers, next); } private boolean isValidToken(String token) { return token != null && token.startsWith("valid-"); } private String extractUserId(String token) { return token.replace("valid-", ""); } } // UserContext.java public class UserContext { public static final Context.Key<String> USER_ID_CTX = Context.key("user-id"); } 2. Interceptor Testing
// AuthenticationInterceptorTest.java import io.grpc.*; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AuthenticationInterceptorTest { private Server server; private ManagedChannel channel; private UserServiceGrpc.UserServiceBlockingStub blockingStub; private final String serverName = InProcessServerBuilder.generateName(); @BeforeEach void setUp() throws Exception { UserRepository userRepository = mock(UserRepository.class); server = InProcessServerBuilder .forName(serverName) .intercept(new AuthenticationInterceptor()) .addService(new UserServiceImpl(userRepository)) .build() .start(); channel = InProcessChannelBuilder .forName(serverName) .build(); blockingStub = UserServiceGrpc.newBlockingStub(channel); } @Test void testInterceptor_ValidToken() { // Given Metadata headers = new Metadata(); headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer valid-user123"); // When & Then - should not throw authentication error // Note: Actual method call would depend on your service implementation assertDoesNotThrow(() -> { // Make authenticated call }); } @Test void testInterceptor_MissingToken() { // Given - no headers set // When & Then StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> { GetUserRequest request = GetUserRequest.newBuilder() .setUserId("test") .build(); blockingStub.getUser(request); }); assertEquals(Status.UNAUTHENTICATED.getCode(), exception.getStatus().getCode()); } @Test void testInterceptor_InvalidToken() { // Given Metadata headers = new Metadata(); headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer invalid-token"); // When & Then StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> { GetUserRequest request = GetUserRequest.newBuilder() .setUserId("test") .build(); blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers)) .getUser(request); }); assertEquals(Status.UNAUTHENTICATED.getCode(), exception.getStatus().getCode()); } } Advanced Testing Scenarios
1. Testing with Timeouts and Retries
// TimeoutAndRetryTest.java import io.grpc.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class TimeoutAndRetryTest { @Mock private UserRepository userRepository; @Test void testWithTimeout() throws Exception { // Given - slow repository when(userRepository.findById(anyString())).thenAnswer(invocation -> { Thread.sleep(2000); // Simulate slow operation return new User("slow-user", "Slow User", "[email protected]", 30); }); UserServiceImpl service = new UserServiceImpl(userRepository); // Create channel with timeout ManagedChannel channel = InProcessChannelBuilder .forName(InProcessServerBuilder.generateName()) .build(); try { UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc .newBlockingStub(channel) .withDeadlineAfter(500, TimeUnit.MILLISECONDS); // 500ms timeout // When & Then GetUserRequest request = GetUserRequest.newBuilder() .setUserId("test") .build(); assertThrows(StatusRuntimeException.class, () -> stub.getUser(request)); } finally { channel.shutdown(); } } @Test void testRetryBehavior() { // Given - repository that fails first time, succeeds second time AtomicInteger callCount = new AtomicInteger(); when(userRepository.findById(anyString())).thenAnswer(invocation -> { if (callCount.incrementAndGet() == 1) { throw new RuntimeException("First call fails"); } return new User("retry-user", "Retry User", "[email protected]", 30); }); UserServiceImpl service = new UserServiceImpl(userRepository); // Test that service handles retries properly // This would depend on your retry configuration assertDoesNotThrow(() -> { // Implementation would depend on your retry strategy }); } } 2. Load Testing gRPC Services
// LoadTest.java import io.grpc.ManagedChannel; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class LoadTest { @Mock private UserRepository userRepository; @Test void testConcurrentRequests() throws Exception { // Given when(userRepository.findById(anyString())).thenAnswer(invocation -> { String userId = invocation.getArgument(0); return new User(userId, "User " + userId, "[email protected]", 25); }); UserServiceImpl service = new UserServiceImpl(userRepository); String serverName = InProcessServerBuilder.generateName(); ManagedChannel channel = InProcessChannelBuilder .forName(serverName) .build(); try { UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc .newBlockingStub(channel); int numThreads = 10; int requestsPerThread = 100; ExecutorService executor = Executors.newFixedThreadPool(numThreads); List<Future<?>> futures = new ArrayList<>(); AtomicInteger successCount = new AtomicInteger(); // When - execute concurrent requests for (int i = 0; i < numThreads; i++) { Future<?> future = executor.submit(() -> { for (int j = 0; j < requestsPerThread; j++) { try { GetUserRequest request = GetUserRequest.newBuilder() .setUserId("user-" + ThreadLocalRandom.current().nextInt(1000)) .build(); stub.getUser(request); successCount.incrementAndGet(); } catch (Exception e) { // Count failures } } }); futures.add(future); } // Wait for all to complete for (Future<?> future : futures) { future.get(10, TimeUnit.SECONDS); } executor.shutdown(); // Then int expectedRequests = numThreads * requestsPerThread; assertEquals(expectedRequests, successCount.get()); verify(userRepository, times(expectedRequests)).findById(anyString()); } finally { channel.shutdown(); } } } Best Practices for gRPC Testing
- Use In-Process Servers: Avoid network overhead in tests
- Mock Dependencies: Isolate the gRPC service logic
- Test All Streaming Types: Cover unary, client streaming, server streaming, and bidirectional streaming
- Verify Error Conditions: Test timeout, authentication, and validation errors
- Use Appropriate Timeouts: Prevent tests from hanging
- Clean Up Resources: Properly shutdown channels and servers
- Test Interceptors Separately: Isolate cross-cutting concerns
// TestUtilities.java public class TestUtilities { public static ManagedChannel createInProcessChannel(String serverName) { return InProcessChannelBuilder .forName(serverName) .directExecutor() // Use direct executor for predictable test execution .build(); } public static Server createInProcessServer(String serverName, BindableService service) { return InProcessServerBuilder .forName(serverName) .directExecutor() .addService(service) .build(); } public static void shutdownChannel(ManagedChannel channel) throws InterruptedException { if (channel != null) { channel.shutdown(); channel.awaitTermination(5, TimeUnit.SECONDS); } } public static void shutdownServer(Server server) throws InterruptedException { if (server != null) { server.shutdown(); server.awaitTermination(5, TimeUnit.SECONDS); } } } gRPC testing in Java requires careful consideration of the asynchronous and streaming nature of gRPC communications. By leveraging the gRPC testing utilities and following these patterns, you can create comprehensive tests that ensure your gRPC services behave correctly under various conditions.