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.

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.

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.

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…

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!