使用 Spek 对 WebFlux 路由器进行单元测试

Unit testing WebFlux Routers with Spek

我已经将 Spring 的 WebFlux 框架与 Kotlin 一起使用了大约一个月,并且一直很喜欢它。当我准备开始使用 WebFlux 和 Kotlin 编写生产代码时,我发现自己很难以一种简单、轻量级的方式对我的路由器进行单元测试。

Spring Test is an excellent framework, however it is heavier weight than what I was wanting, and I was looking for a test framework that was more expressive than traditional JUnit. Something in the vein of JavaScript's Mocha. Kotlin's Spek 完全符合要求。

下面是我如何使用 Spek 对简单路由器进行单元测试的示例。

WebFlux 定义了一个用于构建路由器的优秀 DSL using Kotlin's Type-Safe Builders。虽然语法非常简洁和可读,但如何断言路由功能 bean it returns 已正确配置并不明显,因为客户端代码几乎无法访问其属性。

假设我们有以下路由器:

@Configuration
class PingRouter(private val pingHandler: PingHandler) {
    @Bean
    fun pingRoute() = router {
        accept(MediaType.APPLICATION_JSON).nest {
            GET("/ping", pingHandler::handlePing)
        }
    }
}

我们想要断言,当一个请求与 /ping 路由匹配且具有 application/json 内容 header 时,该请求将传递给我们的处理函数。

object PingRouterTest: Spek({
    describe("PingRouter") {
        lateinit var pingHandler: PingHandler
        lateinit var pingRouter: PingRouter

        beforeGroup {
            pingHandler = mock()

            pingRouter = PingRouter(pingHandler)
        }

        on("Ping route") {
            /*
                We need to setup a dummy ServerRequest who's path will match the path of our router,
                and who's headers will match the headers expected by our router.
             */
            val request: ServerRequest = mock()
            val headers: ServerRequest.Headers = mock()

            When calling request.pathContainer() itReturns PathContainer.parsePath("/ping")
            When calling request.method() itReturns HttpMethod.GET
            When calling request.headers() itReturns headers
            When calling headers.accept() itReturns listOf(MediaType.APPLICATION_JSON)

            /*
                We call pingRouter.pingRoute() which will return a RouterFunction. We then call route()
                on the RouterFunction to actually send our dummy request to the router. WebFlux returns
                a Mono that wraps the reference to our PingHandler class's handler function in a
                HandlerFunction instance if the request matches our router, if it does not, WebFlux will
                return an empty Mono. Finally we invoke handle() on the HandlerFunction to actually call
                our handler function in our PingHandler class.
             */
            pingRouter.pingRoute().route(request).subscribe({ it.handle(request) })

            /*
                If our pingHandler.handlePing() was invoked by the HandlerFunction, we know we properly
                configured our route for the request.
             */
            it("Should call the handler with request") {
                verify(pingHandler, times(1)).handlePing(request)
            }
        }
    }
})