gRPC testing involves verifying both client and server implementations, testing various scenarios, and ensuring the reliability of your gRPC services.
Core Concepts
What is gRPC Testing?
- Unit testing of gRPC services and clients
- Integration testing of gRPC communication
- Testing different gRPC call types (Unary, Streaming)
- Mocking gRPC services for isolated testing
- Performance and load testing
Key Testing Strategies:
- In-process testing: Running gRPC server in test
- Mock servers: Simulating gRPC service behavior
- Test containers: Using real gRPC servers in containers
- Wire mocking: Intercepting gRPC calls
Dependencies and Setup
Maven Dependencies
<properties>
<grpc.version>1.59.0</grpc.version>
<protobuf.version>3.25.1</protobuf.version>
<grpc-spring-boot.version>2.15.0.RELEASE</grpc-spring-boot.version>
<junit.version>5.9.2</junit.version>
<mockito.version>5.5.0</mockito.version>
<testcontainers.version>1.19.1</testcontainers.version>
</properties>
<dependencies>
<!-- gRPC -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-core</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<version>${grpc.version}</version>
<scope>test</scope>
</dependency>
<!-- Protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf.version}</version>
</dependency>
<!-- Spring Boot gRPC -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>${grpc-spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- Test Containers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<!-- Awaitility -->
<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:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Protocol Buffer Definitions
// src/main/proto/user_service.proto
syntax = "proto3";
package com.example.grpc;
option java_package = "com.example.grpc";
option java_multiple_files = true;
// User service definition
service UserService {
// Unary RPC
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc CreateUser (CreateUserRequest) returns (UserResponse);
rpc UpdateUser (UpdateUserRequest) returns (UserResponse);
rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
// Server streaming RPC
rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
// Client streaming RPC
rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming RPC
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
// Request/Response messages
message GetUserRequest {
string user_id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message UpdateUserRequest {
string user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message DeleteUserRequest {
string user_id = 1;
}
message DeleteUserResponse {
bool success = 1;
string message = 2;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message CreateUsersResponse {
int32 created_count = 1;
repeated string user_ids = 2;
}
message UserResponse {
string user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
string created_at = 5;
string updated_at = 6;
}
message ChatMessage {
string user_id = 1;
string message = 2;
int64 timestamp = 3;
}
gRPC Service Implementation
1. User Service Implementation
package com.example.grpc.service;
import com.example.grpc.*;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@GRpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private final Map<String, UserResponse> userStore = new ConcurrentHashMap<>();
private final AtomicInteger userIdCounter = new AtomicInteger(1);
@Override
public void getUser(GetUserRequest request, StreamObserver<UserResponse> responseObserver) {
logger.info("GetUser request for ID: {}", request.getUserId());
String userId = request.getUserId();
UserResponse user = userStore.get(userId);
if (user == null) {
responseObserver.onError(Status.NOT_FOUND
.withDescription("User not found: " + userId)
.asRuntimeException());
return;
}
responseObserver.onNext(user);
responseObserver.onCompleted();
}
@Override
public void createUser(CreateUserRequest request, StreamObserver<UserResponse> responseObserver) {
logger.info("CreateUser request for: {}", request.getEmail());
try {
// Validate request
if (request.getName().isEmpty() || request.getEmail().isEmpty()) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("Name and email are required")
.asRuntimeException());
return;
}
// Check if user already exists
boolean userExists = userStore.values().stream()
.anyMatch(user -> user.getEmail().equals(request.getEmail()));
if (userExists) {
responseObserver.onError(Status.ALREADY_EXISTS
.withDescription("User with email already exists: " + request.getEmail())
.asRuntimeException());
return;
}
// Create user
String userId = "user_" + userIdCounter.getAndIncrement();
UserResponse user = UserResponse.newBuilder()
.setUserId(userId)
.setName(request.getName())
.setEmail(request.getEmail())
.setAge(request.getAge())
.setCreatedAt(String.valueOf(System.currentTimeMillis()))
.setUpdatedAt(String.valueOf(System.currentTimeMillis()))
.build();
userStore.put(userId, user);
logger.info("User created successfully: {}", userId);
responseObserver.onNext(user);
responseObserver.onCompleted();
} catch (Exception e) {
logger.error("Error creating user", e);
responseObserver.onError(Status.INTERNAL
.withDescription("Failed to create user")
.withCause(e)
.asRuntimeException());
}
}
@Override
public void updateUser(UpdateUserRequest request, StreamObserver<UserResponse> responseObserver) {
logger.info("UpdateUser request for ID: {}", request.getUserId());
try {
String userId = request.getUserId();
UserResponse existingUser = userStore.get(userId);
if (existingUser == null) {
responseObserver.onError(Status.NOT_FOUND
.withDescription("User not found: " + userId)
.asRuntimeException());
return;
}
// Update user
UserResponse updatedUser = UserResponse.newBuilder(existingUser)
.setName(request.getName())
.setEmail(request.getEmail())
.setAge(request.getAge())
.setUpdatedAt(String.valueOf(System.currentTimeMillis()))
.build();
userStore.put(userId, updatedUser);
logger.info("User updated successfully: {}", userId);
responseObserver.onNext(updatedUser);
responseObserver.onCompleted();
} catch (Exception e) {
logger.error("Error updating user", e);
responseObserver.onError(Status.INTERNAL
.withDescription("Failed to update user")
.withCause(e)
.asRuntimeException());
}
}
@Override
public void deleteUser(DeleteUserRequest request, StreamObserver<DeleteUserResponse> responseObserver) {
logger.info("DeleteUser request for ID: {}", request.getUserId());
String userId = request.getUserId();
UserResponse removedUser = userStore.remove(userId);
DeleteUserResponse response = DeleteUserResponse.newBuilder()
.setSuccess(removedUser != null)
.setMessage(removedUser != null ?
"User deleted successfully" : "User not found")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void listUsers(ListUsersRequest request, StreamObserver<UserResponse> responseObserver) {
logger.info("ListUsers request - page size: {}", request.getPageSize());
try {
int pageSize = request.getPageSize() > 0 ? request.getPageSize() : 10;
List<UserResponse> users = new ArrayList<>(userStore.values());
// Simulate pagination
int endIndex = Math.min(pageSize, users.size());
List<UserResponse> pageUsers = users.subList(0, endIndex);
for (UserResponse user : pageUsers) {
responseObserver.onNext(user);
// Simulate some processing time
Thread.sleep(100);
}
responseObserver.onCompleted();
logger.info("ListUsers completed, sent {} users", pageUsers.size());
} catch (Exception e) {
logger.error("Error listing users", e);
responseObserver.onError(Status.INTERNAL
.withDescription("Failed to list users")
.withCause(e)
.asRuntimeException());
}
}
@Override
public StreamObserver<CreateUserRequest> createUsers(StreamObserver<CreateUsersResponse> responseObserver) {
logger.info("CreateUsers streaming request started");
return new StreamObserver<CreateUserRequest>() {
private final List<String> createdUserIds = new ArrayList<>();
private int processedCount = 0;
@Override
public void onNext(CreateUserRequest request) {
logger.debug("Processing streaming user creation: {}", request.getEmail());
try {
// Create user
String userId = "user_" + userIdCounter.getAndIncrement();
UserResponse user = UserResponse.newBuilder()
.setUserId(userId)
.setName(request.getName())
.setEmail(request.getEmail())
.setAge(request.getAge())
.setCreatedAt(String.valueOf(System.currentTimeMillis()))
.setUpdatedAt(String.valueOf(System.currentTimeMillis()))
.build();
userStore.put(userId, user);
createdUserIds.add(userId);
processedCount++;
logger.debug("Streaming user created: {}", userId);
} catch (Exception e) {
logger.error("Error in streaming user creation", e);
// Continue processing other requests
}
}
@Override
public void onError(Throwable t) {
logger.error("Error in CreateUsers stream", t);
}
@Override
public void onCompleted() {
logger.info("CreateUsers streaming completed, processed {} users", processedCount);
CreateUsersResponse response = CreateUsersResponse.newBuilder()
.setCreatedCount(createdUserIds.size())
.addAllUserIds(createdUserIds)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
};
}
@Override
public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessage> responseObserver) {
logger.info("Chat bidirectional streaming started");
return new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
logger.debug("Received chat message from {}: {}",
message.getUserId(), message.getMessage());
// Echo the message back with timestamp
ChatMessage response = ChatMessage.newBuilder(message)
.setTimestamp(System.currentTimeMillis())
.build();
responseObserver.onNext(response);
}
@Override
public void onError(Throwable t) {
logger.error("Error in chat stream", t);
}
@Override
public void onCompleted() {
logger.info("Chat streaming completed");
responseObserver.onCompleted();
}
};
}
}
2. gRPC Client Service
package com.example.grpc.client;
import com.example.grpc.*;
import io.grpc.Channel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class UserServiceClient {
private static final Logger logger = LoggerFactory.getLogger(UserServiceClient.class);
private final UserServiceGrpc.UserServiceBlockingStub blockingStub;
private final UserServiceGrpc.UserServiceStub asyncStub;
public UserServiceClient(Channel channel) {
this.blockingStub = UserServiceGrpc.newBlockingStub(channel);
this.asyncStub = UserServiceGrpc.newStub(channel);
}
// Unary RPC methods
public UserResponse getUser(String userId) {
logger.info("Getting user: {}", userId);
try {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(userId)
.build();
return blockingStub.getUser(request);
} catch (StatusRuntimeException e) {
logger.error("RPC failed: {}", e.getStatus());
throw new GrpcClientException("Failed to get user", e);
}
}
public UserResponse createUser(String name, String email, int age) {
logger.info("Creating user: {}", email);
try {
CreateUserRequest request = CreateUserRequest.newBuilder()
.setName(name)
.setEmail(email)
.setAge(age)
.build();
return blockingStub.createUser(request);
} catch (StatusRuntimeException e) {
logger.error("RPC failed: {}", e.getStatus());
throw new GrpcClientException("Failed to create user", e);
}
}
// Server streaming RPC
public List<UserResponse> listUsers(int pageSize) {
logger.info("Listing users with page size: {}", pageSize);
try {
ListUsersRequest request = ListUsersRequest.newBuilder()
.setPageSize(pageSize)
.build();
List<UserResponse> users = new ArrayList<>();
blockingStub.listUsers(request).forEachRemaining(users::add);
logger.info("Retrieved {} users", users.size());
return users;
} catch (StatusRuntimeException e) {
logger.error("RPC failed: {}", e.getStatus());
throw new GrpcClientException("Failed to list users", e);
}
}
// Client streaming RPC
public CreateUsersResponse createUsers(List<CreateUserRequest> userRequests) {
logger.info("Creating {} users via streaming", userRequests.size());
CountDownLatch finishLatch = new CountDownLatch(1);
AtomicReference<CreateUsersResponse> responseRef = new AtomicReference<>();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
StreamObserver<CreateUserRequest> requestObserver =
asyncStub.createUsers(new StreamObserver<CreateUsersResponse>() {
@Override
public void onNext(CreateUsersResponse response) {
responseRef.set(response);
}
@Override
public void onError(Throwable t) {
errorRef.set(t);
finishLatch.countDown();
}
@Override
public void onCompleted() {
finishLatch.countDown();
}
});
try {
// Send all requests
for (CreateUserRequest request : userRequests) {
requestObserver.onNext(request);
}
requestObserver.onCompleted();
// Wait for completion
if (!finishLatch.await(1, TimeUnit.MINUTES)) {
throw new GrpcClientException("CreateUsers timed out");
}
// Check for errors
if (errorRef.get() != null) {
throw new GrpcClientException("CreateUsers failed", errorRef.get());
}
return responseRef.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GrpcClientException("CreateUsers interrupted", e);
}
}
// Bidirectional streaming RPC
public List<ChatMessage> chat(List<ChatMessage> messages) {
logger.info("Starting chat with {} messages", messages.size());
CountDownLatch finishLatch = new CountDownLatch(1);
List<ChatMessage> responses = new ArrayList<>();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
StreamObserver<ChatMessage> requestObserver =
asyncStub.chat(new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
responses.add(message);
}
@Override
public void onError(Throwable t) {
errorRef.set(t);
finishLatch.countDown();
}
@Override
public void onCompleted() {
finishLatch.countDown();
}
});
try {
// Send all messages
for (ChatMessage message : messages) {
requestObserver.onNext(message);
}
requestObserver.onCompleted();
// Wait for completion
if (!finishLatch.await(30, TimeUnit.SECONDS)) {
throw new GrpcClientException("Chat timed out");
}
// Check for errors
if (errorRef.get() != null) {
throw new GrpcClientException("Chat failed", errorRef.get());
}
return responses;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GrpcClientException("Chat interrupted", e);
}
}
}
class GrpcClientException extends RuntimeException {
public GrpcClientException(String message) {
super(message);
}
public GrpcClientException(String message, Throwable cause) {
super(message, cause);
}
}
gRPC Testing Framework
1. Base gRPC Test Configuration
package com.example.grpc.test;
import io.grpc.*;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Base class for gRPC in-process testing
*/
public abstract class GrpcTestBase {
private static final Logger logger = LoggerFactory.getLogger(GrpcTestBase.class);
protected Server server;
protected ManagedChannel channel;
protected String serverName;
@BeforeEach
void setUp() throws IOException {
serverName = InProcessServerBuilder.generateName();
// Create server
server = InProcessServerBuilder.forName(serverName)
.directExecutor()
.addService(createService())
.build()
.start();
// Create channel
channel = InProcessChannelBuilder.forName(serverName)
.directExecutor()
.build();
logger.info("gRPC in-process server started: {}", serverName);
}
@AfterEach
void tearDown() throws InterruptedException {
if (channel != null) {
channel.shutdown();
if (!channel.awaitTermination(5, TimeUnit.SECONDS)) {
channel.shutdownNow();
}
}
if (server != null) {
server.shutdown();
if (!server.awaitTermination(5, TimeUnit.SECONDS)) {
server.shutdownNow();
}
}
logger.info("gRPC in-process server stopped: {}", serverName);
}
/**
* Implement this method to provide the gRPC service for testing
*/
protected abstract BindableService createService();
}
2. Unit Tests for gRPC Service
package com.example.grpc.service;
import com.example.grpc.*;
import com.example.grpc.test.GrpcTestBase;
import io.grpc.BindableService;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest extends GrpcTestBase {
private UserServiceGrpc.UserServiceBlockingStub blockingStub;
private UserServiceImpl userService;
@Override
protected BindableService createService() {
userService = new UserServiceImpl();
return userService;
}
@BeforeEach
void setUpStub() {
blockingStub = UserServiceGrpc.newBlockingStub(channel);
}
@Test
void testGetUser_Success() {
// Given - create a user first
CreateUserRequest createRequest = CreateUserRequest.newBuilder()
.setName("John Doe")
.setEmail("[email protected]")
.setAge(30)
.build();
UserResponse createdUser = blockingStub.createUser(createRequest);
// When
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(createdUser.getUserId())
.build();
UserResponse response = blockingStub.getUser(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getUserId()).isEqualTo(createdUser.getUserId());
assertThat(response.getName()).isEqualTo("John Doe");
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getAge()).isEqualTo(30);
}
@Test
void testGetUser_NotFound() {
// Given
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("non-existent-id")
.build();
// When / Then
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.getUser(request),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.NOT_FOUND.getCode());
assertThat(exception.getStatus().getDescription()).contains("User not found");
}
@Test
void testCreateUser_Success() {
// Given
CreateUserRequest request = CreateUserRequest.newBuilder()
.setName("Jane Smith")
.setEmail("[email protected]")
.setAge(25)
.build();
// When
UserResponse response = blockingStub.createUser(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getUserId()).isNotBlank();
assertThat(response.getName()).isEqualTo("Jane Smith");
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getAge()).isEqualTo(25);
assertThat(response.getCreatedAt()).isNotBlank();
assertThat(response.getUpdatedAt()).isNotBlank();
}
@Test
void testCreateUser_ValidationFailure() {
// Given - missing required fields
CreateUserRequest request = CreateUserRequest.newBuilder()
.setAge(25)
.build();
// When / Then
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.createUser(request),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.INVALID_ARGUMENT.getCode());
assertThat(exception.getStatus().getDescription()).contains("Name and email are required");
}
@Test
void testCreateUser_DuplicateEmail() {
// Given - create first user
CreateUserRequest firstRequest = CreateUserRequest.newBuilder()
.setName("First User")
.setEmail("[email protected]")
.setAge(30)
.build();
blockingStub.createUser(firstRequest);
// When / Then - try to create second user with same email
CreateUserRequest secondRequest = CreateUserRequest.newBuilder()
.setName("Second User")
.setEmail("[email protected]")
.setAge(35)
.build();
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.createUser(secondRequest),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.ALREADY_EXISTS.getCode());
assertThat(exception.getStatus().getDescription()).contains("already exists");
}
@Test
void testUpdateUser_Success() {
// Given - create a user first
CreateUserRequest createRequest = CreateUserRequest.newBuilder()
.setName("Original Name")
.setEmail("[email protected]")
.setAge(30)
.build();
UserResponse createdUser = blockingStub.createUser(createRequest);
// When - update the user
UpdateUserRequest updateRequest = UpdateUserRequest.newBuilder()
.setUserId(createdUser.getUserId())
.setName("Updated Name")
.setEmail("[email protected]")
.setAge(35)
.build();
UserResponse updatedUser = blockingStub.updateUser(updateRequest);
// Then
assertThat(updatedUser).isNotNull();
assertThat(updatedUser.getUserId()).isEqualTo(createdUser.getUserId());
assertThat(updatedUser.getName()).isEqualTo("Updated Name");
assertThat(updatedUser.getEmail()).isEqualTo("[email protected]");
assertThat(updatedUser.getAge()).isEqualTo(35);
assertThat(updatedUser.getUpdatedAt()).isNotEqualTo(createdUser.getUpdatedAt());
}
@Test
void testDeleteUser_Success() {
// Given - create a user first
CreateUserRequest createRequest = CreateUserRequest.newBuilder()
.setName("To Delete")
.setEmail("[email protected]")
.setAge(30)
.build();
UserResponse createdUser = blockingStub.createUser(createRequest);
// When
DeleteUserRequest deleteRequest = DeleteUserRequest.newBuilder()
.setUserId(createdUser.getUserId())
.build();
DeleteUserResponse response = blockingStub.deleteUser(deleteRequest);
// Then
assertThat(response.getSuccess()).isTrue();
assertThat(response.getMessage()).contains("deleted successfully");
// Verify user is actually deleted
GetUserRequest getRequest = GetUserRequest.newBuilder()
.setUserId(createdUser.getUserId())
.build();
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.getUser(getRequest),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.NOT_FOUND.getCode());
}
@Test
void testDeleteUser_NotFound() {
// Given
DeleteUserRequest request = DeleteUserRequest.newBuilder()
.setUserId("non-existent-id")
.build();
// When
DeleteUserResponse response = blockingStub.deleteUser(request);
// Then
assertThat(response.getSuccess()).isFalse();
assertThat(response.getMessage()).contains("not found");
}
@Test
void testListUsers_ServerStreaming() {
// Given - create multiple users
for (int i = 1; i <= 5; i++) {
CreateUserRequest request = CreateUserRequest.newBuilder()
.setName("User " + i)
.setEmail("user" + i + "@example.com")
.setAge(20 + i)
.build();
blockingStub.createUser(request);
}
// When
ListUsersRequest request = ListUsersRequest.newBuilder()
.setPageSize(3)
.build();
List<UserResponse> users = new ArrayList<>();
blockingStub.listUsers(request).forEachRemaining(users::add);
// Then
assertThat(users).hasSize(3);
assertThat(users).allSatisfy(user -> {
assertThat(user.getUserId()).isNotBlank();
assertThat(user.getName()).isNotBlank();
assertThat(user.getEmail()).isNotBlank();
});
}
}
3. Streaming Tests
package com.example.grpc.service;
import com.example.grpc.*;
import com.example.grpc.test.GrpcTestBase;
import io.grpc.BindableService;
import io.grpc.stub.StreamObserver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceStreamingTest extends GrpcTestBase {
private UserServiceGrpc.UserServiceStub asyncStub;
@Override
protected BindableService createService() {
return new UserServiceImpl();
}
@BeforeEach
void setUpStub() {
asyncStub = UserServiceGrpc.newStub(channel);
}
@Test
void testCreateUsers_ClientStreaming() throws InterruptedException {
// Given
List<CreateUserRequest> requests = List.of(
CreateUserRequest.newBuilder()
.setName("User 1")
.setEmail("[email protected]")
.setAge(25)
.build(),
CreateUserRequest.newBuilder()
.setName("User 2")
.setEmail("[email protected]")
.setAge(30)
.build(),
CreateUserRequest.newBuilder()
.setName("User 3")
.setEmail("[email protected]")
.setAge(35)
.build()
);
CountDownLatch finishLatch = new CountDownLatch(1);
AtomicReference<CreateUsersResponse> responseRef = new AtomicReference<>();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
// When
StreamObserver<CreateUserRequest> requestObserver =
asyncStub.createUsers(new StreamObserver<CreateUsersResponse>() {
@Override
public void onNext(CreateUsersResponse response) {
responseRef.set(response);
}
@Override
public void onError(Throwable t) {
errorRef.set(t);
finishLatch.countDown();
}
@Override
public void onCompleted() {
finishLatch.countDown();
}
});
// Send all requests
for (CreateUserRequest request : requests) {
requestObserver.onNext(request);
}
requestObserver.onCompleted();
// Wait for completion
assertTrue(finishLatch.await(5, TimeUnit.SECONDS), "Operation timed out");
// Then
assertThat(errorRef.get()).isNull();
assertThat(responseRef.get()).isNotNull();
assertThat(responseRef.get().getCreatedCount()).isEqualTo(3);
assertThat(responseRef.get().getUserIdsList()).hasSize(3);
}
@Test
void testChat_BidirectionalStreaming() throws InterruptedException {
// Given
List<ChatMessage> messagesToSend = List.of(
ChatMessage.newBuilder()
.setUserId("user1")
.setMessage("Hello")
.setTimestamp(System.currentTimeMillis())
.build(),
ChatMessage.newBuilder()
.setUserId("user1")
.setMessage("How are you?")
.setTimestamp(System.currentTimeMillis())
.build(),
ChatMessage.newBuilder()
.setUserId("user1")
.setMessage("Goodbye")
.setTimestamp(System.currentTimeMillis())
.build()
);
CountDownLatch finishLatch = new CountDownLatch(1);
List<ChatMessage> receivedMessages = new ArrayList<>();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
// When
StreamObserver<ChatMessage> requestObserver =
asyncStub.chat(new StreamObserver<ChatMessage>() {
@Override
public void onNext(ChatMessage message) {
receivedMessages.add(message);
}
@Override
public void onError(Throwable t) {
errorRef.set(t);
finishLatch.countDown();
}
@Override
public void onCompleted() {
finishLatch.countDown();
}
});
// Send all messages
for (ChatMessage message : messagesToSend) {
requestObserver.onNext(message);
}
requestObserver.onCompleted();
// Wait for completion
assertTrue(finishLatch.await(5, TimeUnit.SECONDS), "Operation timed out");
// Then
assertThat(errorRef.get()).isNull();
assertThat(receivedMessages).hasSize(3);
// Verify responses match sent messages
for (int i = 0; i < messagesToSend.size(); i++) {
ChatMessage sent = messagesToSend.get(i);
ChatMessage received = receivedMessages.get(i);
assertThat(received.getUserId()).isEqualTo(sent.getUserId());
assertThat(received.getMessage()).isEqualTo(sent.getMessage());
assertThat(received.getTimestamp()).isGreaterThan(0);
}
}
}
4. gRPC Client Tests
package com.example.grpc.client;
import com.example.grpc.*;
import com.example.grpc.test.GrpcTestBase;
import io.grpc.BindableService;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceClientTest extends GrpcTestBase {
private UserServiceClient client;
private UserServiceGrpc.UserServiceBlockingStub blockingStub;
@Override
protected BindableService createService() {
return new UserServiceImpl();
}
@BeforeEach
void setUpClient() {
client = new UserServiceClient(channel);
blockingStub = UserServiceGrpc.newBlockingStub(channel);
}
@Test
void testGetUser_Success() {
// Given - create a user via blocking stub
CreateUserRequest createRequest = CreateUserRequest.newBuilder()
.setName("Test User")
.setEmail("[email protected]")
.setAge(30)
.build();
UserResponse createdUser = blockingStub.createUser(createRequest);
// When
UserResponse response = client.getUser(createdUser.getUserId());
// Then
assertThat(response).isNotNull();
assertThat(response.getUserId()).isEqualTo(createdUser.getUserId());
assertThat(response.getName()).isEqualTo("Test User");
assertThat(response.getEmail()).isEqualTo("[email protected]");
}
@Test
void testGetUser_NotFound() {
// When / Then
GrpcClientException exception = catchThrowableOfType(
() -> client.getUser("non-existent-id"),
GrpcClientException.class
);
assertThat(exception).hasMessageContaining("Failed to get user");
assertThat(exception.getCause()).isInstanceOf(StatusRuntimeException.class);
}
@Test
void testCreateUser_Success() {
// When
UserResponse response = client.createUser("New User", "[email protected]", 25);
// Then
assertThat(response).isNotNull();
assertThat(response.getName()).isEqualTo("New User");
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getAge()).isEqualTo(25);
}
@Test
void testListUsers_Success() {
// Given - create multiple users
for (int i = 1; i <= 5; i++) {
blockingStub.createUser(CreateUserRequest.newBuilder()
.setName("User " + i)
.setEmail("user" + i + "@example.com")
.setAge(20 + i)
.build());
}
// When
List<UserResponse> users = client.listUsers(3);
// Then
assertThat(users).hasSize(3);
assertThat(users).allSatisfy(user -> {
assertThat(user.getUserId()).isNotBlank();
assertThat(user.getName()).isNotBlank();
assertThat(user.getEmail()).isNotBlank();
});
}
@Test
void testCreateUsers_ClientStreaming() {
// Given
List<CreateUserRequest> requests = List.of(
CreateUserRequest.newBuilder()
.setName("Stream User 1")
.setEmail("[email protected]")
.setAge(25)
.build(),
CreateUserRequest.newBuilder()
.setName("Stream User 2")
.setEmail("[email protected]")
.setAge(30)
.build()
);
// When
CreateUsersResponse response = client.createUsers(requests);
// Then
assertThat(response).isNotNull();
assertThat(response.getCreatedCount()).isEqualTo(2);
assertThat(response.getUserIdsList()).hasSize(2);
}
@Test
void testChat_BidirectionalStreaming() {
// Given
List<ChatMessage> messages = List.of(
ChatMessage.newBuilder()
.setUserId("test-user")
.setMessage("Hello")
.setTimestamp(System.currentTimeMillis())
.build(),
ChatMessage.newBuilder()
.setUserId("test-user")
.setMessage("Test message")
.setTimestamp(System.currentTimeMillis())
.build()
);
// When
List<ChatMessage> responses = client.chat(messages);
// Then
assertThat(responses).hasSize(2);
assertThat(responses.get(0).getMessage()).isEqualTo("Hello");
assertThat(responses.get(1).getMessage()).isEqualTo("Test message");
}
}
5. Mock Server Tests
package com.example.grpc.service;
import com.example.grpc.*;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.testing.GrpcCleanupRule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceMockTest {
private final UserServiceGrpc.UserServiceImplBase serviceImpl =
mock(UserServiceGrpc.UserServiceImplBase.class, delegatesTo(
new UserServiceGrpc.UserServiceImplBase() {
// Default implementations that can be overridden by Mockito
}
));
@Test
void testGetUserWithMock() throws IOException {
// Given
UserServiceGrpc.UserServiceImplBase service =
mock(UserServiceGrpc.UserServiceImplBase.class);
UserResponse mockResponse = UserResponse.newBuilder()
.setUserId("mock-user")
.setName("Mock User")
.setEmail("[email protected]")
.setAge(30)
.setCreatedAt("123456789")
.setUpdatedAt("123456789")
.build();
// Set up mock behavior
doAnswer(invocation -> {
StreamObserver<UserResponse> responseObserver = invocation.getArgument(1);
responseObserver.onNext(mockResponse);
responseObserver.onCompleted();
return null;
}).when(service).getUser(any(GetUserRequest.class), any(StreamObserver.class));
// Create in-process server with mock service
String serverName = InProcessChannelBuilder.generateName();
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
grpcCleanup.register(InProcessServerBuilder
.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start());
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(
grpcCleanup.register(InProcessChannelBuilder
.forName(serverName)
.directExecutor()
.build()));
// When
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("test-user")
.build();
UserResponse response = stub.getUser(request);
// Then
assertThat(response).isEqualTo(mockResponse);
verify(service).getUser(any(GetUserRequest.class), any(StreamObserver.class));
}
@Test
void testGetUserErrorWithMock() throws IOException {
// Given
UserServiceGrpc.UserServiceImplBase service =
mock(UserServiceGrpc.UserServiceImplBase.class);
// Set up mock to throw error
doAnswer(invocation -> {
StreamObserver<UserResponse> responseObserver = invocation.getArgument(1);
responseObserver.onError(Status.NOT_FOUND
.withDescription("User not found")
.asRuntimeException());
return null;
}).when(service).getUser(any(GetUserRequest.class), any(StreamObserver.class));
// Create in-process server with mock service
String serverName = InProcessChannelBuilder.generateName();
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
grpcCleanup.register(InProcessServerBuilder
.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start());
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(
grpcCleanup.register(InProcessChannelBuilder
.forName(serverName)
.directExecutor()
.build()));
// When / Then
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("non-existent")
.build();
assertThatThrownBy(() -> stub.getUser(request))
.isInstanceOf(io.grpc.StatusRuntimeException.class)
.hasMessageContaining("NOT_FOUND: User not found");
verify(service).getUser(any(GetUserRequest.class), any(StreamObserver.class));
}
@Test
void testListUsersStreamingWithMock() throws IOException {
// Given
UserServiceGrpc.UserServiceImplBase service =
mock(UserServiceGrpc.UserServiceImplBase.class);
List<UserResponse> mockResponses = List.of(
UserResponse.newBuilder()
.setUserId("user1")
.setName("User One")
.setEmail("[email protected]")
.setAge(25)
.build(),
UserResponse.newBuilder()
.setUserId("user2")
.setName("User Two")
.setEmail("[email protected]")
.setAge(30)
.build()
);
// Set up mock behavior for streaming
doAnswer(invocation -> {
StreamObserver<UserResponse> responseObserver = invocation.getArgument(1);
for (UserResponse response : mockResponses) {
responseObserver.onNext(response);
}
responseObserver.onCompleted();
return null;
}).when(service).listUsers(any(ListUsersRequest.class), any(StreamObserver.class));
// Create in-process server with mock service
String serverName = InProcessChannelBuilder.generateName();
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
grpcCleanup.register(InProcessServerBuilder
.forName(serverName)
.directExecutor()
.addService(service)
.build()
.start());
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(
grpcCleanup.register(InProcessChannelBuilder
.forName(serverName)
.directExecutor()
.build()));
// When
ListUsersRequest request = ListUsersRequest.newBuilder()
.setPageSize(10)
.build();
List<UserResponse> responses = new ArrayList<>();
stub.listUsers(request).forEachRemaining(responses::add);
// Then
assertThat(responses).hasSize(2);
assertThat(responses).containsExactlyElementsOf(mockResponses);
verify(service).listUsers(any(ListUsersRequest.class), any(StreamObserver.class));
}
}
6. Integration Tests with TestContainers
package com.example.grpc.integration;
import com.example.grpc.UserServiceGrpc;
import com.example.grpc.UserResponse;
import com.example.grpc.CreateUserRequest;
import com.example.grpc.GetUserRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
class UserServiceIntegrationTest {
private static final int GRPC_PORT = 6565;
@Container
private static final GenericContainer<?> grpcServer = new GenericContainer<>(
DockerImageName.parse("my-grpc-server:latest"))
.withExposedPorts(GRPC_PORT)
.withEnv("SERVER_PORT", String.valueOf(GRPC_PORT));
@Test
void testGrpcServerIntegration() {
// Get mapped port
Integer mappedPort = grpcServer.getMappedPort(GRPC_PORT);
String host = grpcServer.getHost();
// Create channel to test container
ManagedChannel channel = ManagedChannelBuilder
.forAddress(host, mappedPort)
.usePlaintext()
.build();
try {
UserServiceGrpc.UserServiceBlockingStub stub =
UserServiceGrpc.newBlockingStub(channel);
// Test create user
CreateUserRequest createRequest = CreateUserRequest.newBuilder()
.setName("Integration User")
.setEmail("[email protected]")
.setAge(35)
.build();
UserResponse createResponse = stub.createUser(createRequest);
assertThat(createResponse).isNotNull();
assertThat(createResponse.getName()).isEqualTo("Integration User");
assertThat(createResponse.getEmail()).isEqualTo("[email protected]");
// Test get user
GetUserRequest getRequest = GetUserRequest.newBuilder()
.setUserId(createResponse.getUserId())
.build();
UserResponse getResponse = stub.getUser(getRequest);
assertThat(getResponse).isNotNull();
assertThat(getResponse.getUserId()).isEqualTo(createResponse.getUserId());
} finally {
channel.shutdown();
}
}
}
7. Performance and Load Testing
package com.example.grpc.performance;
import com.example.grpc.*;
import com.example.grpc.test.GrpcTestBase;
import io.grpc.BindableService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
class UserServicePerformanceTest extends GrpcTestBase {
private UserServiceGrpc.UserServiceBlockingStub blockingStub;
private UserServiceGrpc.UserServiceStub asyncStub;
private ExecutorService executorService;
@Override
protected BindableService createService() {
return new UserServiceImpl();
}
@BeforeEach
void setUpStubs() {
blockingStub = UserServiceGrpc.newBlockingStub(channel);
asyncStub = UserServiceGrpc.newStub(channel);
executorService = Executors.newFixedThreadPool(10);
}
@Test
void testConcurrentUserCreations() throws InterruptedException {
int numberOfRequests = 100;
CountDownLatch latch = new CountDownLatch(numberOfRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
// When - execute concurrent requests
for (int i = 0; i < numberOfRequests; i++) {
final int index = i;
executorService.submit(() -> {
try {
CreateUserRequest request = CreateUserRequest.newBuilder()
.setName("User " + index)
.setEmail("user" + index + "@example.com")
.setAge(20 + (index % 50))
.build();
blockingStub.createUser(request);
successCount.incrementAndGet();
} catch (Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
});
}
// Then - wait for all requests to complete
assertThat(latch.await(30, TimeUnit.SECONDS)).isTrue();
System.out.printf("Performance test completed: %d success, %d errors%n",
successCount.get(), errorCount.get());
assertThat(successCount.get()).isGreaterThan(0);
// Note: Some errors are expected due to duplicate email constraints
}
@Test
void testStreamingPerformance() throws InterruptedException {
int numberOfUsers = 50;
List<CreateUserRequest> requests = new ArrayList<>();
for (int i = 0; i < numberOfUsers; i++) {
requests.add(CreateUserRequest.newBuilder()
.setName("Stream User " + i)
.setEmail("stream" + i + "@example.com")
.setAge(25 + (i % 30))
.build());
}
long startTime = System.currentTimeMillis();
// When - use client streaming
CountDownLatch finishLatch = new CountDownLatch(1);
AtomicInteger responseCount = new AtomicInteger(0);
StreamObserver<CreateUserRequest> requestObserver =
asyncStub.createUsers(new StreamObserver<CreateUsersResponse>() {
@Override
public void onNext(CreateUsersResponse response) {
responseCount.set(response.getCreatedCount());
}
@Override
public void onError(Throwable t) {
finishLatch.countDown();
}
@Override
public void onCompleted() {
finishLatch.countDown();
}
});
for (CreateUserRequest request : requests) {
requestObserver.onNext(request);
}
requestObserver.onCompleted();
// Wait for completion
assertThat(finishLatch.await(10, TimeUnit.SECONDS)).isTrue();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.printf("Streaming performance: %d users in %d ms%n",
responseCount.get(), duration);
assertThat(responseCount.get()).isGreaterThan(0);
assertThat(duration).isLessThan(5000); // Should complete within 5 seconds
}
}
8. Error Handling and Resilience Tests
package com.example.grpc.error;
import com.example.grpc.*;
import com.example.grpc.test.GrpcTestBase;
import io.grpc.BindableService;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.assertj.core.api.Assertions.*;
class UserServiceErrorTest extends GrpcTestBase {
private UserServiceGrpc.UserServiceBlockingStub blockingStub;
// Service that throws various exceptions for testing
static class ErrorProneUserService extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(GetUserRequest request, StreamObserver<UserResponse> responseObserver) {
if ("timeout".equals(request.getUserId())) {
// Simulate timeout by not responding
return;
} else if ("internal-error".equals(request.getUserId())) {
responseObserver.onError(Status.INTERNAL
.withDescription("Internal server error")
.asRuntimeException());
} else if ("invalid-argument".equals(request.getUserId())) {
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("Invalid user ID")
.asRuntimeException());
} else {
responseObserver.onError(Status.NOT_FOUND
.withDescription("User not found: " + request.getUserId())
.asRuntimeException());
}
}
}
@Override
protected BindableService createService() {
return new ErrorProneUserService();
}
@BeforeEach
void setUpStub() {
blockingStub = UserServiceGrpc.newBlockingStub(channel);
}
@Test
void testInternalError() {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("internal-error")
.build();
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.getUser(request),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.INTERNAL.getCode());
assertThat(exception.getStatus().getDescription()).contains("Internal server error");
}
@Test
void testInvalidArgumentError() {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("invalid-argument")
.build();
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.getUser(request),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.INVALID_ARGUMENT.getCode());
assertThat(exception.getStatus().getDescription()).contains("Invalid user ID");
}
@Test
void testNotFoundError() {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId("any-user")
.build();
StatusRuntimeException exception = catchThrowableOfType(
() -> blockingStub.getUser(request),
StatusRuntimeException.class
);
assertThat(exception.getStatus().getCode()).isEqualTo(Status.NOT_FOUND.getCode());
assertThat(exception.getStatus().getDescription()).contains("User not found");
}
}
Test Configuration
# src/test/resources/application-test.yml grpc: server: port: 0 # Use random port for tests in-process-name: test-server logging: level: com.example.grpc: DEBUG io.grpc: WARN
Best Practices
- Test All RPC Types: Cover unary, client streaming, server streaming, and bidirectional streaming
- Error Scenarios: Test various error conditions and status codes
- Concurrent Testing: Test under concurrent load
- Mock Appropriately: Use mocks for external dependencies
- Integration Testing: Test with real servers when possible
- Performance Testing: Include performance benchmarks
// Example of comprehensive test annotations
@GrpcTest
@ExtendWith({MockitoExtension.class, GrpcCleanupExtension.class})
@ActiveProfiles("test")
@Tag("grpc")
@Tag("integration")
class ComprehensiveGrpcTest {
// Tests covering all scenarios
}
Conclusion
gRPC testing in Java provides:
- Comprehensive Coverage: Unit, integration, and performance testing
- Multiple Testing Strategies: In-process, mock servers, TestContainers
- Streaming Support: Full testing of streaming RPCs
- Error Handling: Testing various gRPC status codes and error scenarios
- Performance Validation: Load and concurrent testing capabilities
This comprehensive testing approach ensures your gRPC services are reliable, performant, and handle all edge cases correctly. The combination of in-process testing for fast unit tests and container-based testing for integration scenarios provides a robust testing strategy for gRPC-based microservices.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.