grpc-javaのClient/ServerのテストをKotlinで書く - Server編
grpc-javaで実装されたgRPC ClientとgRPC Serverのテストコードについてまとめていきたい。
ClientとServerのどちらも大枠は同じである。テストコードのなかでgRPC Serverを起動させる。そしてリクエスト内のトランザクションを必要に応じてモック化しながら期待値が取得できているか、期待される関数が呼び出せれているかを検証する。
今回のエントリではServer側のテストをJUnitとKotlinを用いてまとめていく。
テスト対象のproto
テスト対象のprotoは次のとおりSimple-RPCとする。
service TaskService { rpc GetTaskService (TaskInbound) returns (TaskOutbound) { option (google.api.http) = { get: "/v1/task" }; } } message TaskInbound { uint32 task_id = 1; } message TaskOutbound { uint32 task_id = 1; string title = 2; string finishedAt = 3; string createdAt = 4; string updatedAt = 5; }
テストするgRPC Serverとテスト内容
テスト対象のServerのコードは次のとおりである
override fun getTaskService(request: TaskInbound?, responseObserver: StreamObserver<TaskOutbound>?) { try { val (taskId) = GRpcInboundValidator.validTaskInbound(request) val log = GRpcLogContextHandler.getLog() log.elem { "taskId" to taskId } val task = getTaskService(GetTaskCommand(taskId.toLong())) val msg = getOutbound(task) responseObserver?.onNext(msg) responseObserver?.onCompleted() } catch (e: WebAppException.NotFoundException) { logger.error { "gRPC server error, task not found." } responseObserver?.onError( Status.NOT_FOUND.withDescription("task not found.").asRuntimeException()) } catch (e: WebAppException.BadRequestException) { logger.error { "gRPC server error, invalid request." } responseObserver?.onError( Status.INVALID_ARGUMENT.withDescription("invalid request.").asRuntimeException()) } }
taskId: Int
をパラメータにとるTaskBackendServerでTaskBackendServer#getTaskService
はタスクを1件返す。GRpcLogContextHandler
はリクエストパラメータをログ出力するためにio.grpc.Context
にログ情報を詰めこむ。TaskBackendServer#getTaskService
の中でエラーが発生した場合は、エラーに応じたgRPC Serverレスポンスを返す。
テスト内容
テストする内容を次のようにまとめる。
TaskBackendServer#getTaskService
を呼び出すとタスクが1件取得できるか。GRpcLogContextHandler.getLog()
は正常に呼び出されているか。TaskBackendServer#getTaskService
の中で発生したエラーに応じたgRPC Serverレスポンスが返ってくるか。
テストコード
次にテストコードである。
先述したとおりテストコードのなかでServerを起動させる。そして起動しているServerにテスト対象のgRPC Sereverをアサインする。コードとしては次のようになる。
@Before fun setUp() { getTaskService = mock(GetTaskServiceImpl::class) // 一部省略 target = TaskBackendServer(getTaskService, getTaskListService, createTaskService, updateTaskService, deleteTaskService, finishTaskService) inProcessServer = InProcessServerBuilder .forName(UNIQUE_SERVER_NAME).addService(target).directExecutor().build() inProcessChannel = InProcessChannelBuilder.forName(UNIQUE_SERVER_NAME).directExecutor().build() inProcessServer.start() } @After fun tearDown() { inProcessChannel.shutdownNow() inProcessServer.shutdownNow() }
@Before
でInProcessServerBuilder
を使いServerをビルドアップしている。ビルドしたServerにTaskBackendServer
をaddService
関数を使いアサインする。(addService(target)
)InProcessServerBuilder
で起動したServerはUNIQUE_SERVER_NAME
という名称をつけている。このServerNameをInProcessChannelBuilder
でビルドアップするChannelに関連付ける。- ビルドアップしたChannel(
inProcessChannel
)をテストコードでブロッキングすることでgRPC Serverのレスポンスを受け取ることができる。
正常系のテスト
次のコードは正常系をテストしたコードである。
@Test fun getProducts_onCompleted() { val taskId = 1L val request = TaskInbound.newBuilder() .setTaskId(taskId.toInt()) .build() val command = GetTaskCommand(taskId) val now = LocalDateTime.now() val task = Task(taskId.toInt(), "mocked Task", now, now, now) val log = GRpcLogBuilder() // mock mockStatic(GRpcLogContextHandler::class) Mockito.`when`(GRpcLogContextHandler.getLog()).thenReturn(log) Mockito.`when`(getTaskService(command)).thenReturn(task) // request server val blockingStub = TaskServiceGrpc.newBlockingStub(inProcessChannel) val actual = blockingStub.getTaskService(request) // ブロッキングしてgRPC Serverのレスポンスを受け取る // assertion actual.taskId shouldBe 1 actual.title shouldBe "mocked Task" }
- このテストコードでは期待したタスクが
TaskBackendServer
から返ってきているか、GRpcLogContextHandler.getLog()
が呼び出せれているかを検証している TaskBackendServer#getTaskService
内ではGetTaskServiceImpl#invoke
を呼び出しタスクを1件取得している。この処理をモック化することでテストコード内のgRPC Serverの挙動をコントロールしている。
異常系のテスト
次のコードは異常系をテストしたコードである。
@Test fun getProducts_NOT_FOUND() { val taskId = 1L val request = TaskInbound.newBuilder().setTaskId(taskId.toInt()).build() val command = GetTaskCommand(taskId) // mock mockStatic(GRpcLogContextHandler::class) Mockito.`when`(GRpcLogContextHandler.getLog()).thenReturn(GRpcLogBuilder()) Mockito.`when`(getTaskService(command)).thenThrow(WebAppException.NotFoundException("not found")) try { // request server val blockingStub = TaskServiceGrpc.newBlockingStub(inProcessChannel) blockingStub.getTaskService(request) } catch (e: StatusRuntimeException) { // assertion e.status.code shouldBe Status.NOT_FOUND.code e.message shouldBe "NOT_FOUND: task not found." } } @Test fun getProducts_INVALID_ARGUMENT() { val taskId = 0L val request = TaskInbound.newBuilder().setTaskId(taskId.toInt()).build() try { // request server val blockingStub = TaskServiceGrpc.newBlockingStub(inProcessChannel) blockingStub.getTaskService(request) } catch (e: StatusRuntimeException) { // assertion e.status.code shouldBe Status.INVALID_ARGUMENT.code e.message shouldBe "INVALID_ARGUMENT: invalid request." } }
- タスクが存在しない(NotFound)、リクエストパラメータが不整合(INVALID_ARGUMENT)のテストコードである。
responseObserver
のonError
にエラーがセットされるとStatusRuntimeException
が発生する。テストコードでそれをキャッチしエラーコードとエラーメッセージを検証している。
まとめ
- gRPC Serverのテストコードをまとめた。
- テストコード内でgRPC Serverを起動させる方法とテストコードでChannel をブロッキングしgRPC Serverのレスポンスを受け取る方法を紹介した。
- 必要に応じてモック化することでgRPC Serverのテストカバレッジを向上させることができる。
- 次回のエントリではgRPC Clientのテスト方法をまとめていく。
コード
エントリで紹介したコードは一部分のためコード全体はgithubを参照してください。
テストコードはこちらです。