在 gRPC 服務中實現單元測試
1.概述
gRPC 是一個高效能、開源的 RPC 框架,它由 HTTP/2 協定支持,可以更直接地實作 API 模式定義並自動產生樣板存根程式碼。
由於 gRPC 服務的底層結構(例如伺服器、通道和序列化),對其進行單元測試可能稍微複雜一些。
在本教程中,我們將學習如何用 Java 實作 gRPC 服務。我們還將使用 gRPC 框架提供的支援編寫單元測試。
2.實現 gRPC 服務
假設我們需要建立一個 gRPC 服務,向客戶端傳回一些資料。
2.1. Maven 依賴項
我們將包括與 gRPC 相關的grpc-netty-shaded
、 [grpc-protobuf](https://mvnrepository.com/artifact/io.grpc/grpc-protobuf)
和grpc-stub
依賴項:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<scope>runtime</scope>
<version>1.75.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.75.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.63.0</version>
</dependency>
接下來,我們將定義 API 契約。
2.2. 使用Proto
檔案定義服務
RPC 服務、請求和回應模式將使用協定緩衝區檔案來定義。
首先,我們將在user_service.proto
檔案中定義UserService契約:
syntax = "proto3";
package userservice;
option java_multiple_files = true;
option java_package = "com.baeldung.grpc.userservice";
service UserService {
rpc GetUser(UserRequest) returns (UserResponse);
}
在上面的程式碼中,我們定義了 RPC GetUser
服務,以UserRequest
作為輸入,以UserResponse
作為輸出。
然後,我們將在同一個檔案中定義UserRequest、UserResponse和User
訊息模式:
message UserRequest {
int32 id = 1;
}
message UserResponse {
User user = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
我們應該注意,該屬性的值是 gRPC 用於序列化的標籤或序號。
2.3. 產生存根類
我們將從user_service.proto
檔案自動產生 gRPC 存根和請求/回應類別。
我們將protobuf-maven-plugin
加入現有的pom.xml
檔中:
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
protobuf-maven-plugin
插件將在專案的目標資料夾中產生所需的類別。
2.4. 實現服務
我們需要在UserServiceImplBase
基底類別中實作getUser
方法。此方法將傳回從儲存庫中取得的使用者。
此外,與任何生產環境一樣,預計會出現一些邊緣情況,例如未找到用戶。我們可以透過拋出定義RuntimeException
以及相關的 gRPC NOT_FOUND
狀態碼和描述來處理這種情況。 gRPC 有一組狀態碼,其意義與 HTTP 狀態碼類似。
讓我們重寫UserServiceImplBase類別中定義的getUser
方法:
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
try {
User user = Optional.ofNullable(userRepositoryMap.get(request.getId()))
.orElseThrow(() -> new UserNotFoundException(request.getId()));
UserResponse response = UserResponse.newBuilder()
.setUser(user)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
logger.info("Return User for id {}", request.getId());
} catch (UserNotFoundException ex) {
responseObserver.onError(Status.NOT_FOUND.withDescription(ex.getMessage()).asRuntimeException());
}
}
在上面的程式碼中,我們從地圖儲存庫中檢索使用者並將其設定在StreamObserver
物件中。
此外,我們將實作自訂的UserNotFoundException
類別:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(int userId) {
super(String.format("User not found with ID %s", userId));
}
}
接下來,我們將測試上述 gRPC 服務。
3.測試 gRPC 服務
我們需要在 gRPC 伺服器上下文中測試getUser
服務,從而測試序列化/反序列化、攔截器和訊息傳播。
3.1. Maven依賴
為了便於測試,gRPC 在測試庫中提供了進程內/記憶體中的Server
和Channel
實作.
我們將在pom.xml
檔中加入grpc-testing
測試依賴項:
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<version>1.75.0</version>
<scope>test</scope>
</dependency>
接下來,我們將使用庫中提供的支援來設定測試類別。
3.2. 單元測試的設置
grpc-testing
庫提供了InProcessServerBuilder
和InProcessChannelBuilder
類別來支援測試。
首先,讓我們使用Server
和ManagedChannel
實例以及UserServiceBlockingStub
類別來寫一個測試類別:
public class UserServiceUnitTest {
private UserServiceGrpc.UserServiceBlockingStub userServiceBlockingStub;
private Server inProcessServer;
private ManagedChannel managedChannel;
@BeforeEach
void setup() throws IOException {
String serviceName = InProcessServerBuilder.generateName();
inProcessServer = InProcessServerBuilder.forName(serviceName)
.directExecutor()
.addService(new UserServiceImpl())
.build()
.start();
managedChannel = InProcessChannelBuilder.forName(serviceName)
.directExecutor()
.usePlaintext()
.build();
userServiceBlockingStub = UserServiceGrpc.newBlockingStub(managedChannel);
}
}
在上面的程式碼中,我們將實際服務UserServiceImpl
物件加入Server
實例中,然後將ManagedChannel
物件賦值給UserServiceBlockingStub
類別。
需要注意的是,伺服器在同一個測試進程中運行,並且不涉及任何套接字或 TCP 開銷。因此,測試運行速度快、可靠,並且獨立於網路堆疊。
3.3. 單元測試的實施
我們將實現用戶存在於服務中的測試場景。
讓我們使用有效的id
呼叫getUser
方法並驗證回應:
@Test
void givenUserIsPresent_whenGetUserIsCalled_ThenReturnUser() {
UserRequest userRequest = UserRequest.newBuilder()
.setId(1)
.build();
UserResponse userResponse = userServiceBlockingStub.getUser(userRequest);
assertNotNull(userResponse);
assertNotNull(userResponse.getUser());
assertEquals(1, userResponse.getUser().getId());
assertEquals("user1", userResponse.getUser().getName());
assertEquals("[email protected]", userResponse.getUser().getEmail());
}
我們可以測試用戶不存在的情況:
@Test
void givenUserIsNotPresent_whenGetUserIsCalled_ThenThrowRuntimeException(){
UserRequest userRequest = UserRequest.newBuilder()
.setId(1000)
.build();
StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> userServiceBlockingStub.getUser(userRequest));
assertNotNull(statusRuntimeException);
assertNotNull(statusRuntimeException.getStatus());
assertEquals(Status.NOT_FOUND.getCode(), statusRuntimeException.getStatus().getCode());
assertTrue(statusRuntimeException.getStatus().getDescription().contains("User not found with ID 1000"));
}
在上面的斷言中,我們已經驗證了異常包含前面定義的NOT_FOUND
狀態代碼和description
。
我們應該注意到, gRPC 實作將自訂的UserNotFoundException
轉換為客戶端的StatusRuntimeException
。
4.實現 gRPC 客戶端
客戶端程式碼將使用從user_service.proto
檔案產生的存根UserServiceBlockingStub
類別。
首先,我們將在UserClient
類別中建置UserServiceBlockingStub
和ManagedChannel
:
public class UserClient {
private final UserServiceGrpc.UserServiceBlockingStub userServiceStub;
private final ManagedChannel managedChannel;
public UserClient(ManagedChannel managedChannel, UserServiceGrpc.UserServiceBlockingStub userServiceStub) {
this.managedChannel = managedChannel;
this.userServiceStub = UserServiceGrpc.newBlockingStub(managedChannel);
}
}
然後,我們在UserClient
類別中加入getUser
方法:
public User getUser(int id) {
UserRequest userRequest = UserRequest.newBuilder()
.setId(id)
.build();
return userServiceStub.getUser(userRequest).getUser();
}
在上面的程式碼中,我們呼叫存根的getUser
方法,就像呼叫本機方法一樣,但實際上它是一個遠端過程呼叫。
5.測試客戶端
現在,我們可以像之前在伺服器端程式碼中那樣測試UserClient
類別了。不過,我們需要提供一個模擬的或偽造的UserServiceImplBase
服務.
首先,我們將使用Server
、 ManagedChannel、模擬的UserServiceImplBase和UserClient
編寫測試setup
方法:
@BeforeEach
public void setup() throws Exception {
String serverName = InProcessServerBuilder.generateName();
mockUserService = spy(UserServiceGrpc.UserServiceImplBase.class);
inProcessServer = InProcessServerBuilder
.forName(serverName)
.directExecutor()
.addService(mockUserService.bindService())
.build()
.start();
managedChannel = InProcessChannelBuilder.forName(serverName)
.directExecutor()
.usePlainText()
.build();
userClient = new UserClient(managedChannel);
}
然後,讓我們加入一個輔助方法來使用 Mockito 模擬GetUser
方法:
private void mockGetUser(User expectedUser) {
Mockito.doAnswer(invocation -> {
StreamObserver<UserResponse> observer = invocation.getArgument(1);
UserResponse response = UserResponse.newBuilder()
.setUser(expectedUser)
.build();
observer.onNext(response);
observer.onCompleted();
return null;
}).when(mockUserService).getUser(any(), any());
}
最後,我們來實現用戶存在的測試案例:
@Test
void givenUserIsPresent_whenGetUserIsCalled_ThenReturnUser() {
User expectedUser = User.newBuilder()
.setId(1)
.setName("user1")
.setEmail("[email protected]")
.build();
mockGetUser(expectedUser);
User user = userClient.getUser(1);
assertEquals(expectedUser, user);
}
我們將運行上述測試案例並確認斷言通過。
6. 結論
在本教程中,我們學習如何使用.proto
檔案在 Java 中實作 gRPC 服務。我們也使用 gRPC 框架提供的記憶體Server
和ManagedChannel
實例在伺服器端和客戶端實現了單元測試。
與往常一樣,範例程式碼可以在 GitHub 上找到。