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.