Why I avoid writing integration tests in WebFlux and Kotlin

frustrated spring boot 2

I won’t be messing around. I love Kotlin and I like writing enterprise applications using Spring. And from my previous post you can draw conclusions that Kotlin and Spring is a great combo to work with. And, indeed, it is! Guys from Pivotal did a really great job to support this new programming language in their framework. But this time, it’s both the Spring’s team and the JetBrains team that needs to make some improvements in the codebase so we could properly write… integration tests.

[15 MAY 2018] UPDATE: As of Spring Boot 2.0.2/Spring 5.0.6, WebTestClient is now usable in Kotlin. Examples in this changeset.

Spring WebFlux’s Test annotation

I wanted to contain this in the previous post, but the struggles I faced deserve their own one.
In my last post I’ve showed you how you can implement a simple reactive service using Kotlin and Spring Boot 2 with a new web framework – Spring WebFlux. And everything had been going okay until I started writing integration tests. Well, to be honest, I couldn’t even start because the basic configuration exceeded my limits of patience.

@ExtendWith(SpringExtension::class)
@WebFluxTest
class ReactivekotlinApplicationTests {

    @Autowired
    private lateinit var client: WebTestClient

    @Test
    fun getBooks() {
        client.get().uri("http://localhost:8080/books")
        .exchange().expectStatus().isOk
    }

}

The above snippet is a basic configuration for testing WebFlux apps. My test method just wants to send a simple GET request and check if returned HTTP status is equal to 200.
I’m executing this test and.. exception is thrown.

webflux webtestclient 404 not found

It looks like the test context can’t see my endpoints. Quick look at the documentation of @WebFluxTest and I know that I need to pass a controller class that I want to test. But I have no controller. I’m using the new functional routing DSL. So I can’t use @WebFluxTest. One of the flag functionalities of the release and no one cared about adding support fot testing it the easy way?

Manual configuration of WebTestClient

Fortunately, we can abandon autowiring the WebTestClient field and just manually point it to the correct router configuration bean. With addition of the new JUnit5’s @BeforeAll annotation I can set everything up before my tests start execution:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@WebFluxTest
internal class ReactivekotlinApplicationTests {

    @Autowired
    private lateinit var routing: Routing

    @Autowired
    private lateinit var booksHandler: BooksHandler

    private lateinit var client: WebTestClient

    @BeforeAll
    fun initClient() {
        client = WebTestClient.bindToRouterFunction(routing.booksRouter(booksHandler)).build()
    }

    @Test
    fun getBooks() {
        client.get().uri("http://localhost:8080/books")
                .exchange().expectStatus().isOk
    }

}

And another long stacktrace of exceptions in being thrown. This time I was treated with UnsatisfiedDependencyException and NoSuchBeanDefinitionException regarding my Routing configuration. I can’t really inject @Configuration anywhere – this annotation is used as an indicator for Spring that this class declares @Beans definitions.

So, what’s coming next?

Good ol’ @SpringBootTest

@SpringBootTest to the rescue. Or not 🙂 Let’s revert everything to the first snippet and put this annotation in the place of @WebFluxTest and see what happens.

autowired webtestclient

I don’t even have to run it. The compiler tells me straightaway that there’s no WebTestClient bean configured. It was the @WebFluxTest‘s job to define it. So I put @WebFluxTest back and run the test…

java.lang.IllegalStateException: Configuration error: found multiple declarations of @BootstrapWith for test class [net.amarszalek.reactivekotlin.ReactivekotlinApplicationTests]: [@org.springframework.test.context.BootstrapWith(value=class org.springframework.boot.test.context.SpringBootTestContextBootstrapper), @org.springframework.test.context.BootstrapWith(value=class org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTestContextBootstrapper)]

I’m starting to get annoyed even though I’m a naturally calm person.

Me during writing this post
Me during writing this post

Back to manually setting up WebClient…

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@SpringBootTest
internal class ReactivekotlinApplicationTests {

    private lateinit var client: WebTestClient

    @BeforeAll
    fun initClient() {
        client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
    }

    @Test
    fun getBooks() {
        client.get().uri("http://localhost:8080/books")
                .exchange().expectStatus().isOk
    }

}

And execute…

Netty connection refused

Now I’m starting wondering “why me, why me?”. In the logs it looks like everything’s okay – the application started perfectly, the router has been mapped. But still… Every request I make is being refused.

At this point, my energy depleted to 0. I started Googling for this exception and everything that comes up is about Minecraft. It seems like it’s a popular bug in there. Oh God. I think I’m going to download Minecraft. This is what I need right now.

(5 hours later)

Okay, I killed some creepers. Refreshed and relaxed I can get back to integration tests.

Getting rid of class annotations

After hours spent on Googling and trying to make it work I decided to give up on autoconfiguration test annotations provided by Spring. Almost completely. Instead, I switched to manually booting up the application and guess what. It worked. Yes. I’m surprised too.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ReactivekotlinApplicationTests {

    private lateinit var client: WebTestClient

    @BeforeAll
    fun initClient() {
        runApplication<ReactivekotlinApplication>()
        client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
    }

    @Test
    fun getBooks() {
        client.get().uri("http://localhost:8080/books")
                .exchange().expectStatus().isOk
    }
}

Note that the @BeforeAll annotation runs just once before all test methods. You can for example extract it to a base class that’s going to setup your test environment. That’s what I did.
But after a moment of playing with my code I realized another thing. I’m not in a valid Spring bean, so I can’t inject any components. Therefore I can’t prepare test data in database or whatever else I imagine. You know what’s the solution? 🙂 Bringing back the annotations. That’s what we have now:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest
@ExtendWith(SpringExtension::class)
class TestBase {

    protected val client = WebTestClient.bindToServer()
            .baseUrl("http://127.0.0.1:8080")
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build()

    @BeforeAll
    fun initApplication() {
        runApplication<ReactivekotlinApplication>()
    }
}

class ReactivekotlinApplicationTests : TestBase() {

    @Autowired
    private lateinit var repository: BookRepository

    @Test
    fun getBooks() {
        client.get().uri("/books/{title}", "Title5")
                .exchange().expectStatus().isOk
    }
}

The test passes. Finally. I missed the green.

The battle is not over yet

Now I should further develop my test class with data preparation, assertions etc., but there’s another issue in Spring WebFlux. Or rather in Kotlin.

Kotlin is unable to inherit types from WebTestClient. There are issues reported to Spring’s JIRA: SPR-15692 and to Kotlin’s YouTrack: KT-5464.

With this bug not resolved, we can’t use expectations methods from the BodySpec interface. And there’s no proper workaround, except using the ordinary WebClient instead of the version designated for tests. The target of fix is set to Kotlin 1.3. Right now all you can do with WebTestClient is assert json values:

@Test
fun getBooks() {
    val bookTitle = "Title5"

    client.get().uri("/books/{title}", bookTitle)
            .exchange().expectStatus().isOk
            .expectBody()
            .jsonPath("$.title").isEqualTo(bookTitle)
}

Conclusion (and disclaimer)

When it comes to developing reactive web application, I’m going to abandon Kotlin. At times I was at one’s wits’ end while writing this post. Java together with Spring WebFlux is no less powerful than Kotlin. I hope no one denies that 😉

It’s time to wind down. At the end of this post I just wanted to point out that it’s just a rant. It’s not personal and I really appreciate the work both from Spring’s team (especially @sdeleuze) and JetBrains’ team. Keep it up!

10 thoughts on “Why I avoid writing integration tests in WebFlux and Kotlin

  1. Sébastien Deleuze Reply

    I have created https://github.com/sdeleuze/webflux-kotlin-web-tests/ in order to show you how to write tests with various use cases.

    Your @SpringBootTest example can be much simpler and use WebTestClient injection via @AutoConfigureWebTestClient. @WebFluxTest with functional router requires using @Import, see https://github.com/spring-projects/spring-boot/issues/10683. Be aware that you can use .returnResult().responseBody.apply as a workaround for KT-5464.

    I have reopened https://jira.spring.io/browse/SPR-15692 in order to provide an extension providing this workaround in an easier way. I will also improve WebTestClient documentation to mention accordingly.

  2. Elmo Reply

    You could´ve just started the server with for example:

    @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

    And then bind the WebTestClient with for example:
    @LocalServerPort
    val port: Int = 0

    WebTestClient.bindToServer().baseUrl(“http://localhost:$port”)

  3. hiper2d Reply

    I faced the same problems as you, reached the end of your article (thanks for it btw) and finally found in comments the @AutoConfigureWebTestClient annotation which did the trick. That’s crazy.

  4. steve Reply

    Great post! trying to work on a similar example but replacing the reactive guts with akka.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.