grpc-javaのClient/ServerのテストをKotlinで書く - Client編
前回のエントリに続いて今回のエントリではgRPC Clientのテストの書き方をまとめていく。
テスト対象の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; }
finishedAt
などは本来であればgoogle/protobuf/timestamp.proto
を使いたいところが今回はstring
で定義している。
テストするgRPC Cleintとテスト内容
テスト対象のClientのコードは次のとおりである。
suspend fun getTask(taskId: Long): TaskOutbound = async(CommonPool) { try { val outbound = ShutdownLoan.using(getChannel(), { channel -> val msg = TaskInbound.newBuilder().setTaskId(taskId.toInt()).build() TaskServiceGrpc.newBlockingStub(channel).getTaskService(msg) }) Result.Success<TaskOutbound, GrpcException>(outbound) } catch (e: Exception) { val status = Status.fromThrowable(e) logger.error(e) { "gRPC server error, code:{%d}, description:{%s}".format(status.code.value(), status.description) } Result.Failure<TaskOutbound, GrpcException>(status with status.description) } }.await().fold({ it }, { throw it }) private fun getChannel() = NettyChannelBuilder.forAddress(appProperties.grpc.backend.host, appProperties.grpc.backend.port) // for testing .usePlaintext(true) .build()
コルーチンをつかっているが、通常のgRPC Serverへリクエストするクライアントコードである。
val msg = TaskInbound.newBuilder().setTaskId(taskId.toInt()).build()
でリクエスト変数を定義している。TaskServiceGrpc.newBlockingStub(channel).getTaskService(msg)
でgRPC serverへリクエストをしている。- gRPC Serverから受け取ったレスポンスを
Result
型に格納し返却する。 getChannel()
はgRPCのチャネルを返すメソッドである。
テスト内容
テストする内容を次のようにまとめる。
getTask(taskId: Long)
を呼び出すとタスクが1件取得できるか。TaskServiceGrpc.newBlockingStub(channel).getTaskService(msg)
ではgRPC Serverへリクエストが渡るがリクエスト結果がエラーだった場合に例外エラー(GrpcException
)を受け取ることができるか。
テストコード
次にテストコードである。
前回のエントリでまとめたとおりテストコードのなかでServerを起動させる。Clientのテストではテスト内容に応じて 成功を返すgRPC Server Service
、エラーを返すgRPC Server Service
を用意する。そして起動しているServerにテスト対象のgRPC Sereverをアサインする。コードとしては次のようになる。
@Before fun setup() { serviceRegistry = MutableHandlerRegistry() inProcessServer = InProcessServerBuilder .forName(UNIQUE_SERVER_NAME).fallbackHandlerRegistry(serviceRegistry).directExecutor().build() inProcessChannel = InProcessChannelBuilder.forName(UNIQUE_SERVER_NAME).directExecutor().build() val appProperties = AppProperties() target = TaskBackendClient(appProperties) inProcessServer.start() } @After fun shutdown() { inProcessChannel.shutdownNow() inProcessServer.shutdownNow() }
@Before
、After
はServerのテストのエントリでまとめたようにサーバーの起動と停止を行っている。
次のコードが重要である。
private class GetTaskServerOk: TaskServiceGrpc.TaskServiceImplBase() { override fun getTaskService(request: TaskInbound?, responseObserver: StreamObserver<TaskOutbound>?) { responseObserver?.onNext(TaskOutbound.newBuilder() .setTaskId(1) .setTitle("mocked Task") .setFinishedAt("2017-01-01T23:59:59Z") .setCreatedAt("2017-01-02T23:59:59Z") .setUpdatedAt("2017-01-02T23:59:59Z") .build() ) responseObserver?.onCompleted() } }
このGetTaskServerOk
はテストするgRPC Serverが正常なレスポンスを返すServiceクラスである。
このServiceクラスをテストで起動したgRPC Serverにアサインすることでレスポンスをモックできる。
正常系のテスト
次にメインとなるテストコードをまとめる。こちらは正常系のテストである
@Test fun getTask() { serviceRegistry.addService(GetTaskServerOk()) // mock val instance = PowerMockito.spy(target) PowerMockito.doReturn(inProcessChannel).`when`(instance, "getChannel") runBlocking { // assertion val actual = instance.getTask(1L) actual.taskId shouldBe 1 actual.title shouldBe "mocked Task" actual.finishedAt shouldBe "2017-01-01T23:59:59Z" actual.createdAt shouldBe "2017-01-02T23:59:59Z" actual.updatedAt shouldBe "2017-01-02T23:59:59Z" } }
serviceRegistry.addService(GetTaskServerOk())
でモック化したServiceクラスをgRPC Serverに追加している。PowerMockito.doReturn(inProcessChannel).
when(instance, "getChannel")
では、ClientコードにあったgetChannel()
をモック化している。getChannel()
はgRPCのチャネルを返す関数であったが、テストコード内でモック化することでテストでビルドアップしたチャネルを適応している。
異常系のテスト
次に異常系のテストである。gRPC ServerからNot Foundのエラーを返すことを期待したテストコードである。
private class GetTaskServerNotFound : TaskServiceGrpc.TaskServiceImplBase() { override fun getTaskService(request: TaskInbound?, responseObserver: StreamObserver<TaskOutbound>?) { responseObserver?.onError(Status.NOT_FOUND.withDescription("task not found.").asRuntimeException()) responseObserver?.onCompleted() } } @Test(expected = GrpcException::class) fun getTask_then_NotFound() { serviceRegistry.addService(GetTaskServerNotFound()) // mock val instance = PowerMockito.spy(target) PowerMockito.doReturn(inProcessChannel).`when`(instance, "getChannel") try { runBlocking { instance.getTask(1L) } } catch (e: GrpcException) { e.message shouldBe "task not found." e.status shouldBe HttpStatus.NOT_FOUND throw e } }
- エラーを返す
GetTaskServerNotFound
クラスを定義してgRPC Serverにアサインしている。 - テストコードではエラーが発生しているか、エラーメッセージ、コードが期待どおりかテストをしている。
まとめ
- Server編とClient編に分けてgRPC ServerとClientのテストコードをまとめた。
- 今回は
Simple-RPC
をまとめたが次の機会にはServerSideStreaming-RPC
やClientSideStreaming-RPC
、BidirectionalStreaming-RPC
のStreamingのテストをまとめていきたい。
コード
エントリで紹介したコードは一部分のためコード全体はgithubを参照してください。
テストコードはこちらです。