gRPC serverのmetadataをテストする方法をまとめる
gRPC ServerのレスポンスにはMetadataを含めることができる。gRPCのレスポンス・ステータス(io.grpc.Status.OK
やio.grpc.Status.INVALID_ARGUMENT
)やDescription(エラーメッセージ)に加え、例えば400系のエラーでも異なるエラー内容をクライアントに伝えるのに役立つ。HTTP/1.1ではHTTPステータスに加えレスポンス・ボディにJSON形式でエラーメッセージとエラーコードを含めるようなことで実現していたが、gRPCではMetadataを活用すればDescription(エラーメッセージ)をJSON形式にする必要はない。
今回のエントリではMetadataを含むレスポンスを返すgRPC Serverのテスト方法をまとめていく。
Metadataを含むレスポンスを返すgRPC Serverを準備する
gRPC Serverで起きたエラーをインターセプトしてレスポンスを返すInterceptorを以前のエントリで紹介した。
このインターセプターで返すレスポンスにcustom_status
というKeyのMetadataを含めるようにした。コードとしては次のようにしている。
private fun <ReqT, RespT> handleException(call: ServerCall<ReqT, RespT>?, headers: Metadata?, ex: Exception) { when (ex) { // ・・・省略 is WebAppException.NotFoundException -> { Metadata.Key.of("custom_status", Metadata.ASCII_STRING_MARSHALLER).let { headers?.put(it, HttpStatus.NOT_FOUND.value().toString()) } call?.close(Status.fromCode(Status.NOT_FOUND.code).withDescription(ex.message), headers) } // ・・・省略 } }
WebAppException.NotFoundException
のExceptionクラスであればcustom_status
のMetadataには404
の文字列が含まれるようにした。
metadataをテストする方法
metadataをテストするためにはテストコードで起動しているgRPC Serverのインターセプターに新しいインターセプター・クラスを加える。そのインターセプター・クラスのmetadataをキャプチャすることでmetadataが含まれているか検証する。
コードとしては次のようになる。
private val metadataCaptor = ArgumentCaptor.forClass(io.grpc.Metadata::class.java) private val mockServerInterceptor = Mockito.spy(TestInterceptor()) private class TestInterceptor : ServerInterceptor { override fun <ReqT : Any?, RespT : Any?> interceptCall(call: ServerCall<ReqT, RespT>?, headers: io.grpc.Metadata?, next: ServerCallHandler<ReqT, RespT>?): ServerCall.Listener<ReqT> { return next!!.startCall(call, headers) } } @Before fun setUp() { delegateTaskService = mock(DelegateTaskService::class) target = TaskBackendServer(delegateTaskService) inProcessServer = InProcessServerBuilder .forName(UNIQUE_SERVER_NAME) .addService(target) .intercept(exceptionInterceptor) .intercept(mockServerInterceptor) // このインターセプター・クラスのmetadataをキャプチャする .directExecutor() .build() inProcessChannel = InProcessChannelBuilder.forName(UNIQUE_SERVER_NAME).directExecutor().build() inProcessServer.start() }
テストコードは次のようになった。
@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`(delegateTaskService.getTask(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: not found" // metadataの`custom_status`が"404" であること Mockito.verify(mockServerInterceptor).interceptCall( MockHelper.any<ServerCall<TaskInbound, TaskOutbound>>(), metadataCaptor.capture(), MockHelper.any<ServerCallHandler<TaskInbound, TaskOutbound>>()) metadataCaptor.value.get( Metadata.Key.of("custom_status", Metadata.ASCII_STRING_MARSHALLER)) shouldBe "404" } }
MockitoのArgumentCaptor
を活用すればmetadataの検証も手軽に行うことができた。
コード
githubにコードがありますので合わせて参照ください。
テストコードはこちらです。