Search code examples
kotlinspring-webfluxspek

Unit testing WebFlux Routers with Spek


I have been using Spring's WebFlux framework with Kotlin for about a month now, and have been loving it. As I got ready to make the dive into writing production code with WebFlux and Kotlin I found myself struggling to unit test my routers in a simple, lightweight way.

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 fit the bill perfectly.

What follows below is an example of how I was able to unit test a simple router using Spek.


Solution

  • WebFlux defines an excellent DSL using Kotlin's Type-Safe Builders for building routers. While the syntax is very succinct and readable it is not readily apparent how to assert that the router function bean it returns is configured properly as its properties are mostly inaccessible to client code.

    Say we have the following router:

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

    We want to assert that when a request comes in that matches the /ping route with an application/json content header the request is passed off to our handler function.

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