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のテスト方法をまとめていく。
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をテストするためにはテストコードで起動している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にコードがありますので合わせて参照ください。
テストコードはこちらです。