Spring5.0 + KotlinのRouterFunctionのテストはどうすればよいか? 試してみた

引き続きSpring5.0(Spring Boot 2.0) + KotlinのWebアプリケーションを試している。今回はRouterFunctionをつかってHTTPルーティングを定義したときにテストコードはどう書くのか?気になったのでまとめてみた。

Router Function

Router FunctionはSpring5.0から導入されるものでアノテーションベースのHTTPサービスの定義ではなく RouterFunctionDslクラスを実装していくことになる。エンドポイントのURLとHTTP Handlerをマッピングを行うことでルーティングが定義できる。

@Configuration
class TaskRoutes(private val taskHandler: TaskHandler, private val exceptionFilter: ExceptionFilter) {

    @Bean
    fun taskRouter() = router {
        (accept(APPLICATION_JSON) and "/api").nest {
            "/task".nest {
                POST("/", taskHandler::create)
                GET("/{id}", taskHandler::fetchByTaskId)
                PUT("/{id}", taskHandler::updateByTaskId)
                DELETE("/{id}", taskHandler::deleteByTaskId)
                PUT("/{id}/finish", taskHandler::finishByTaskId)
            }
            "/tasks".nest {
                GET("/", taskHandler::fetchAll)
            }
        }
    }.filter(exceptionFilter())
}

次からはRouter Functionで定義されたHTTPサービスのテストコードを書いていきたい。

MockMvcではなくWebTestClient をつかう

SpringのコントローラテストコードはMockMvcを用いて書くことが多かったがRouter Functionでは WebTestClientをつかう。
上記の taskRouterはタスクエンティティを扱うコントローラでありルーティングも固有のBeanで定義している。また TaskHandlerはDBアクセスやgRPC サーバなどロジックが絡むのでモック化していきたい。そのような場合にはWebTestClientに用意されている bindToRouterFunctionを定義する。

@RunWith(SpringRunner::class)
class TaskRoutesTest {
    lateinit var client : WebTestClient
    lateinit var taskHandler: TaskHandler
    lateinit var exceptionFilter: ExceptionFilter

    val mapper = ObjectMapper().registerModule(KotlinModule())

    @Before
    fun before() {
        taskHandler = mock(TaskHandler::class)
        exceptionFilter = ExceptionFilter()

        val taskRoutes = TaskRoutes(taskHandler, exceptionFilter)

        client = WebTestClient.bindToRouterFunction(taskRoutes.taskRouter()).build()
    }

・・・

}
  • TaskHandlerをモック化する(taskHandler変数を定義)
  • taskHandler変数とexceptionFilter変数から TaskRoutesのコンストラクタを呼び出しRoutesオブジェクトを生成する(taskRoutes変数を定義)
  • taskRoutes変数を bindToRouterFunctionに渡しWebTestClientオブジェクトを生成する

以上をbeforeに定義してテスト実行の準備を整える

@Testを定義する

タスクを1件取得するエンドポイントURLのテストコードは次のようになった。

@Test
fun `GET Task`() {

    // mock
    `when`(taskHandler.fetchByTaskId(any())).thenReturn(ok().json().body(Mono.just(mockModel)))

    client.get().uri("/api/task/1")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .exchange()
            .expectStatus().isOk
            .expectBody()
            .consumeAsStringWith {
                val actual = mapper.readValue<TaskModel>(it, TaskModel::class)
                actual.id shouldBe 1L
                actual.title shouldBe "task title"
                actual.createdAt shouldBe "2017-06-13T16:22:52Z"
                actual.updatedAt shouldBe "2017-06-13T16:22:52Z"
            }
}
  • taskHandler.fetchByTaskIdをモック化して返すオブジェクトを定義している
  • exchange()ResponseSpecオブジェクトが返ってくるのでHTTPステータスやレスポンスボディを取得しアサーションする

Spring REST Docsは使えるの?

Test DrivenにAPIドキュメントを生成するSpring Rest DocsはMockMVCベースなので今のところは対応していない。

github.com

対応はしていないが対応はする予定である。issueから確認できるがREST Docs 2.0で対応予定のようなので期待したい。

Add support for using WebTestClient to document an API · Issue #384 · spring-projects/spring-restdocs · GitHub

同じくAPIドキュメントをアノテーションベースで生成できる便利なSpring Foxもサポート予定であることをissueから観測した。

Spring 5 support · Issue #1773 · springfox/springfox · GitHub

個人的にはエンドポイント設計時からアノテーションを付与すればAPI仕様書がつくれるSpring Foxのほうが好みである。だがアノテーションベースで定義する手法が、HTTPルーティングをアノテーションベースで定義しないRouter Functionにどのようにマッピングされるのだろうか。サポート結果が楽しみである。

まとめ

  • RouterFunctionをつかった場合でもテストコードはモックも交えることができるし問題無さそうで導入に前向きになれた。
  • APIドキュメントのライブラリのサポート体制状況も観測できたので、Spring5.0のリリースが待ち遠しい。

コード

コードを書きながら調べたのでgithubと合わせて確認いただけます。

github.com

テストコードはこちらです。 spring5-kotlin-application/TaskRoutesTest.kt at master · nsoushi/spring5-kotlin-application · GitHub

参考にしたエントリ