grpc-javaのClient/ServerのテストをKotlinで書く - Client編


前回のエントリに続いて今回のエントリではgRPC Clientのテストの書き方をまとめていく。

grpc-javaのClient/ServerのテストをKotlinで書く - Server編 - 平日インプット週末アウトプットぶろぐ

テスト対象のproto

テスト対象のprotoは次のとおりSimple-RPCとする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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のコードは次のとおりである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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へリクエストするクライアントコードである。

テスト内容

テストする内容を次のようにまとめる。

テストコード

次にテストコードである。

前回のエントリでまとめたとおりテストコードのなかでServerを起動させる。Clientのテストではテスト内容に応じて 成功を返すgRPC Server Serviceエラーを返すgRPC Server Serviceを用意する。そして起動しているServerにテスト対象のgRPC Sereverをアサインする。コードとしては次のようになる。

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

次のコードが重要である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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にアサインすることでレスポンスをモックできる。

正常系のテスト

次にメインとなるテストコードをまとめる。こちらは正常系のテストである

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@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"
    }
}

異常系のテスト

次に異常系のテストである。gRPC ServerからNot Foundのエラーを返すことを期待したテストコードである。

 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
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
    }
}

まとめ

コード

エントリで紹介したコードは一部分のためコード全体はgithubを参照してください。

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