gRPC serverのmetadataをテストする方法をまとめる


gRPC ServerのレスポンスにはMetadataを含めることができる。gRPCのレスポンス・ステータス(io.grpc.Status.OKio.grpc.Status.INVALID_ARGUMENT)やDescription(エラーメッセージ)に加え、例えば400系のエラーでも異なるエラー内容をクライアントに伝えるのに役立つ。HTTP/1.1ではHTTPステータスに加えレスポンス・ボディにJSON形式でエラーメッセージとエラーコードを含めるようなことで実現していたが、gRPCではMetadataを活用すればDescription(エラーメッセージ)をJSON形式にする必要はない。

今回のエントリではMetadataを含むレスポンスを返すgRPC Serverのテスト方法をまとめていく。

Metadataを含むレスポンスを返すgRPC Serverを準備する

gRPC Serverで起きたエラーをインターセプトしてレスポンスを返すInterceptorを以前のエントリで紹介した。

gRPC ServerのExceptionFilterの方法をまとめた(grpc-java) - 平日インプット週末アウトプットぶろぐ

このインターセプターで返すレスポンスにcustom_statusというKeyのMetadataを含めるようにした。コードとしては次のようにしている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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が含まれているか検証する。

コードとしては次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()
}

テストコードは次のようになった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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にコードがありますので合わせて参照ください。

テストコードはこちらです。