Updated at 25 Feb 2021

Despite this blog post being created recently, I wanted to stick with the same library version as in the blog post that precedes it so that there are no changes due to API evolution. However, I’ve created an up-to-date branch where code from this blog post is migrated to the new Spring Data R2DBC API and I’ll do my best to keep it up-to-date.

Overview

In the last blog post, we’ve seen how to use Project Reactor and Spring Webflux to create a reactive Spring application that exposes an endpoint, fetches some data from a database, calls third-party API to get the additional information, and then returns everything as a JSON. In this post, we will look at how can we test each of those components, but also how can we test altogether. Since this will be the last post in the series of posts about the reactive paradigm in Spring, here’s the list of all previous posts if you’ve missed them and want to gain a better understanding of the content presented in this post:

Before we get started, we will need the latest version of the sample project from the previous post which is available here. One difference from the previous post to this one is that there will be only one new branch for all the tests - there will still be multiple steps that we will go through, but you’d usually do them in one go so it makes sense to bundle everything together.

First steps

You might notice that our whole application resides in one file. Granted, it’s only 70 lines of code, but it’s a sample application and you will never have a case like that in the real life. Also, having everything in one place hides application layers and ties them together making any future changes potentially costly. I’m not going to talk in details about how to incorporate clean architecture or domain-driven design into this application since that’s a topic for another blog post, but we will end up with something that resembles clean architecture, is separated by layers, and is easily testable which should always be an end goal.

To start separating an application into layers, we have to understand what our application does and what it uses to achieve that. The easiest way to understand that is to look at the use-cases that our application contains. In our case, there’s only one - fetch all languages from a database, fill in details from the third-party API and return that result to the user. From this, we can extract few layers: a web layer that exposes the API for others to interact with the application, a domain logic layer that it getting data from different sources and combines them, a database layer that talks with a database, and a component that talks to third-party API. To translate that into Spring terms, we have controller, service, repository, and some sort of an HTTP client for API calls. This will be our starting point for refactoring our one-file application to multiple components which is necessary if we want to be able to test layers of the application without too much trouble.

Controller

Controller extraction is probably the simplest one - we take only a declaration of the @GetMapping method and move it to a controller class.

@RequestMapping("/languages")
@RestController
class LanguagesController {

    @GetMapping
    fun getLanguages(): Flux<Language> {

    }
}
@RequestMapping("/languages")
@RestController
class LanguagesController {

    @GetMapping
    fun getLanguages(): Flux<Language> {

    }
}

Service

To fix the error in the controller, we will need a component that interacts with a database and external API. Create a class LanguagesService, annotate it with @Service, and copy implementation of the getLanguages method along with model and dependencies.

private val urls = mapOf(
    "Kotlin" to "https://run.mocky.io/v3/6654273e-456d-40ce-9209-c879c93a844d",
    "Java" to "https://run.mocky.io/v3/f04c90fc-7529-4f0b-a9d7-90e92105d5bf",
    "Go" to "https://run.mocky.io/v3/2eece29e-29d8-4d59-aa81-9d47886f2e17",
    "Python" to "https://run.mocky.io/v3/72bbad6e-2127-4f8a-a9c1-2fdf15b0c18c"
)

@Service
class LanguagesService(
    private val webClient: WebClient,
    private val databaseClient: DatabaseClient
) {

    fun getLanguages(): Flux<Language> {
        return databaseClient.select().from("languages").`as`(Language::class.java).fetch().all()
            .flatMap { language ->
                // fetch published year for each language
                val languageYearResponse = webClient
                    .get()
                    .uri(urls[language.name].toString())
                    .retrieve()
                    .bodyToMono(LanguageYear::class.java)

                // update language model with a fetched year
                languageYearResponse
                    .map { languageYear -> language.copy(year = languageYear.year) }
            }
    }
}

data class Language(
    val name: String,
    val year: Int? = null
)

data class LanguageYear(
    val year: Int
)

Now, we can fix controller error by injecting and calling the service method:

@RequestMapping("/languages")
@RestController
class LanguagesController(
    private val languagesService: LanguagesService
) {

    @GetMapping
    fun getLanguages(): Flux<Language> {
        return languagesService.getLanguages()
    }
}

Repository and WebClient

Since service class will usually contain your main business logic, you want to cover it with pure unit tests meaning there should be no external dependencies - just pure Java/Kotlin code. For that, we have to remove database and WebClient dependencies. For both of those, we can use interfaces that we will be able to mock in our tests.

For repository, we can have an interface and its implementation in the same file since it’s really short, but you can also extract it to multiple files (if you are using Java, you have to):

interface LanguageRepository {
    fun fetchLanguages(): Flux<Language>
}

@Repository
class ReactiveLanguageRepository(
    private val databaseClient: DatabaseClient
) : LanguageRepository {

    override fun fetchLanguages(): Flux<Language> {
        return databaseClient.select().from("languages").`as`(Language::class.java).fetch().all()
    }
}

With this in place, we can update our service:

@Service
class LanguagesService(
    private val webClient: WebClient,
    private val languageRepository: LanguageRepository
) {
    fun getLanguages(): Flux<Language> {
        return languageRepository.fetchLanguages()
            .flatMap { language ->
                // fetch published year for each language
                val languageYearResponse = webClient
                    .get()
                    .uri(urls[language.name].toString())
                    .retrieve()
                    .bodyToMono(LanguageYear::class.java)

                // update language model with a fetched year
                languageYearResponse
                    .map { languageYear -> language.copy(year = languageYear.year) }
            }
    }
}

One cool thing to notice here is that we don’t care about the storage type in our service layer – this is extremely useful if a project is starting and the storage type is still unknown. You can build your whole application logic using only an interface and then plug in real implementation at the end. Also, changing storage type (or even using two in parallel) is completely transparent from the standpoint of the service itself.

For WebClient we can do a similar thing as for a database:

private val urls = mapOf(
    "Kotlin" to "https://run.mocky.io/v3/6654273e-456d-40ce-9209-c879c93a844d",
    "Java" to "https://run.mocky.io/v3/f04c90fc-7529-4f0b-a9d7-90e92105d5bf",
    "Go" to "https://run.mocky.io/v3/2eece29e-29d8-4d59-aa81-9d47886f2e17",
    "Python" to "https://run.mocky.io/v3/72bbad6e-2127-4f8a-a9c1-2fdf15b0c18c"
)

interface LanguageYearClient {
    fun fetchLanguageYear(language: Language): Mono<LanguageYear>
}

@Component
class MockyLanguageYearClient(
    private val webClient: WebClient
) : LanguageYearClient {

    override fun fetchLanguageYear(language: Language): Mono<LanguageYear> {
        return webClient
            .get()
            .uri(urls[language.name].toString())
            .retrieve()
            .bodyToMono(LanguageYear::class.java)
    }
}

One thing worth mentioning here is that a good idea is to separate your database models or third-party API models from your domain models. They usually contain information you don’t need or you have to somehow transform it to make it useful. So, in a real-world scenario, you’d map Language and LanguageYears in the implementation itself and expose the domain model in your interface(s).

With that in place, here’s our final version of the service:

@Service
class LanguagesService(
    private val languageYearClient: LanguageYearClient,
    private val languageRepository: LanguageRepository
) {

    fun getLanguages(): Flux<Language> {
        return languageRepository.fetchLanguages()
            .flatMap { language ->
                languageYearClient.fetchLanguageYear(language)
                    .map { languageYear -> language.copy(year = languageYear.year) }
            }
    }
}

And that’s it! We have our application separated by layers that we can separately test using Spring’s slice testing capabilities and pure JUnit tests.

Testing

When it comes to testing in general, you usually start with unit tests that cover the smallest scope (a single class or a module) and are fast. This enables you to run them separately from other, bulkier tests in your CI/CD pipeline and quickly see if the application still works as expected. One step above the unit tests are integration tests that usually start the whole application and test the whole flow including things like request handling, third-party API calls, database interactions, etc. Those tests can overlap with the unit tests but are much more reliable due to the scope. When it comes to choosing which tests to write, opinions are split - some people prefer to have a large number of unit tests and not that many integration tests while others prefer more integration tests. In this section, we will see how to do both :)

Of course, those types of tests are not the only ones. There are end-to-end (E2E) tests, system tests, performance tests, functional tests, etc. but we won’t cover those here. They usually require other frameworks, tools, and even a different skillset, while unit and integration tests are part of the standard development workflow.

Apart from unit and integration tests, Spring enables you to do so-called slice testing. This means that, instead of starting the whole Spring application context, the testing framework will start only one layer (e.g. controller layer) and enable you to issue normal HTTP requests to your endpoints while everything else is mocked. This is useful to test request/response payloads as well as error responses. Slice testing capabilities are explained in more detail here so I will skip explanation in this post, but we will touch some parts of it as we test our application. More specifically, we will see how to use WebFlux slice testing capabilities. Of course, controller slice is not the only one that is available, but it’s the one we will be using in this blog post. A list of all other slices can be found here (in a sidebar).

Finally, for our integration tests, we will need a connection to a database. This is sometimes done using an in-memory instance of a database, but this is not a good solution since an in-memory database usually differs from the one used in production. This means that nobody guarantees us that the behavior we tested and verify will work in production. Ideally, we want to have the same environment in our test as well as in the production and this is possible to achieve with the library called Testcontainers.

Controller-level tests

Let’s start from the top - when a request is made, the controller is one of the first components that will be reached in the application (there’s a lot of framework-specific code that runs before the controller is reached). Let’s see how can slice testing on a controller level helps us here.

In the src/main/test, following the package structure of the source, we will create an empty class called LanguagesControllerTest and annotate it with @WebFluxTest which will auto-configure only controller-level beans. The most important bean is WebTestClient that allows us to perform requests to our endpoints and assert received response.

First, let’s create some mock data. To make things shorter, we can define it as a pure Kotlin code, but it can also be read from a file or generated in any other way.

val languages = listOf(
    Language(name = "Test language 1", year = 1000),
    Language(name = "Test language 2", year = 2000),
    Language(name = "Test language 3", year = 3000)
)

Next, we need dependencies that we will use in our tests: already mentioned WebTestClient and the mock of our LanguageService.

@Autowired
lateinit var webTestClient: WebTestClient

@MockBean
lateinit var languagesService: LanguagesService

With the first test, we want to test that controller will return 200 and the data that the service returns. For that, we need to mock service response using Mockito framework. Also, we will be leveraging JUnit 5 @DisplayName feature to give our test descriptive names while keeping method names short. Notice that I’ve mentioned JUnit - those tests are nothing more than a simple unit test with some additional Spring configuration.

@Test
@DisplayName("Getting all languages should return 200 with the list of all languages from the service")
fun getAllLanguagesTest() {
    `when`(languagesService.getLanguages()).thenReturn(Flux.fromIterable(languages))

    webTestClient
        .get()
        .uri("/languages")
        .exchange()
        .expectStatus().isOk
        .expectBodyList<Language>()
        .contains(*languages.toTypedArray())
}

NOTE: If you are new to Kotlin, when is surrounded by backticks because it’s one of Kotlin’s keywords. In Java, there are no backticks.

So, we can see that the first thing we have to do is mock our service response so that it returns mocked languages we’ve defined. Next, we have to specify an endpoint we want to test along with the method, and then we can trigger the call using the exchange method. If your endpoint needs specific headers, attributes, cookies, etc. there are methods to set those before calling exchange, but we don’t need any of that. After a call to exchange, we can check the response: status, headers, and body. For the body, there are multiple options, but here we can use a simple list of assertions that will check if the response contains all the items in the provided set of arguments (contains methods accepts so called vararg so we have to convert the list to it).

If we run this test, it should be green. After that, we can change assertions so that the test fails to make sure the test it’s working. Although this might sound weird, breaking tests on purpose is a good practice to make sure assertions we are using are doing their job.

Next, we want to test the unhappy scenario where the service returns an error.

@Test
@DisplayName("Getting all languages should throw an error if service throws an error")
fun getAllLanguagesErrorTest() {
    `when`(languagesService.getLanguages()).thenReturn(Flux.error(Throwable("My custom error")))

    webTestClient
        .get()
        .uri("/languages")
        .exchange()
        .expectStatus().is5xxServerError
        .expectBody()
        .jsonPath(".status")
        .isEqualTo(500)
        .jsonPath(".error")
        .isEqualTo("Internal Server Error")
}

Similar to the first test, we first have to mock the language service response, but this time it’s some kind of an error. We could be more specific here if we need to test multiple custom errors that the service can return, but since we haven’t set up a proper error handling, we will simply return some random Throwable. The next steps are the same as in the happy scenario, but assertions are a bit different. As I said, we haven’t set up a proper error handling, so error format will be Spring’s default that looks like this:

{
 "error": "Internal Server Error",
 "message": null,
 "path": "/languages",
 "requestId": "04445bdd-1",
 "status": 500,
 "timestamp": "2020-08-06T01:33:44.762+00:00"
}

We can see that there’s a requestId and a timestamp meaning it’s hard to match the whole JSON similarly to how we did it with the language’s response. Luckily, Spring gives us the option to inspect parts of the JSON using JSONPath. With it, we check the status and error values and confirm they are what we expect them to be.

And that’s it when it comes to controller-level tests. In a real project, this is the place where you would test if controllers are properly handling invalid request bodies, missing headers, query parameters, or improperly formatted path parameters (e.g. UUID) and if responses that they produce are following a specification.

Unit testing

We’ve seen unit testing in action above and it will be very similar when done on a service level. What’s important to notice here is that our service does not depend on any Spring dependencies (we can ignore @Service since it’s used for dependency injection) which means our unit (or, rather, JUnit) tests do not require any specific annotations or injected dependencies. The only thing that is different from a unit test in codebases that do not use reactive programming is how to trigger execution and assert that received data is correct.

Before we start writing our first unit test, we need to add testing support for Reactor:

testImplementation("io.projectreactor:reactor-test")

The basic outline of our LanguagesService class will look like this:

class LanguagesServiceTest {

    private val languageRepository: LanguageRepository = mock(LanguageRepository::class.java)
    private val languageYearClient: LanguageYearClient = mock(LanguageYearClient::class.java)

    lateinit var languagesService: LanguagesService

    @BeforeEach
    fun setUp() {
        languagesService = LanguagesService(languageYearClient, languageRepository)
    }

    @Test
    @DisplayName("Get languages should return combination of stored names and fetched years")
    fun getLanguagesTest() {

    }
}

We declare service dependencies as mocks, before each test we create a fresh instance of a LangaguesService and we define a method for our first test, again, leveraging JUnit 5 @DisplayName.

Next step would be to mock return data for a repository and a client:

`when`(languageRepository.fetchLanguages()).thenReturn(Flux.fromIterable(languages))
`when`(languageYearClient.fetchLanguageYear(any())).thenReturn(Mono.just(LanguageYear(2000)))

There’s nothing Reactor-specific here, just a simple mocking using a Mockito framework that ships with Spring. Now, we can finally execute the method we want to test and check if the results are correct. The full test now looks like this:

@Test
@DisplayName("Get languages should return combination of stored names and fetched years")
fun getLanguagesTest() {
    `when`(languageRepository.fetchLanguages()).thenReturn(Flux.fromIterable(languages))
    `when`(languageYearClient.fetchLanguageYear(any())).thenReturn(Mono.just(LanguageYear(2000)))

    val expectedLanguages = listOf(
        Language(name = "Test language 1", year = 2000),
        Language(name = "Test language 2", year = 2000),
        Language(name = "Test language 3", year = 2000)
    )
    StepVerifier.create(languagesService.getLanguages())
        .expectNextSequence(expectedLanguages)
        .verifyComplete()
}

So, instead of calling a method, storing a result to a variable, and then asserting whatever conditions we want, we use StepVerifier.create to define what method we want to tests, and we use a few of the methods from expect* family and finalizing a test with a call to verify. This might not be obvious, but if you check verifyComplete() it’s a combination of two calls expectComplete() and verify(). There’s a bunch of other methods to verify expected results, you can create your publisher to control when data is emitted and you can even control time if you are testing time-sensitive operations. More about all those listed topics can be found in the offical documentation.

NOTE: any() used in the code above is from mockito-kotlin library since Mockito’s matchers do not work properly with Kotlin. For Java, anything from Mockito will work perfectly fine.

Integration testing

The idea of the integration testing is to start all the components of your application and test the whole flow starting with an HTTP call and executing all actions on those components. Those tests give us more confidence that our service works as it should, but are also much slower than pure unit tests. So, how can we start all components of our application and perform validations on them? When it comes to the Spring application, there’s @SpringBootTest annotation that can start the whole application, create all the beans and prepare them for injection. The only thing left is to figure out how to use a database that will be used in the production.

Testcontainers

Testcontainers is a library that enables you to run any dependency your application needs as a Docker container giving it an ability to use the same environment that will be used in production.

Before I continue forward, I have to say that Sergei (one of the top contributors) is active in the community (StackOverflow, Slack, Twitter, conferences) and he has few good resources about this library as well as the whole reactive programming paradigm so check him out :)

Since Testcontainers is a topic that can span through multiple blog posts, I’ll try and keep it as short and as simple as possible. We will go through a simple example that will start up a PostgreSQL database and keep it running for the whole test suite (one class) that will consist of multiple integration tests verifying the functionality of our application.

To start using Testcontainers with PostgreSQL database with R2DBC and JUnit 5, we need these dependencies:

testImplementation("org.testcontainers:testcontainers:1.15.2")
testImplementation("org.testcontainers:junit-jupiter:1.15.2")
testImplementation("org.testcontainers:postgresql:1.15.2")
testImplementation("org.testcontainers:r2dbc:1.15.2")

and now we can create a full integration test suite step-by-step and explain what each of those dependencies gives us.

Test setup

Let’s start by simply defining a test class:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class LanguagesIntegrationTest {

}

We have a standard Spring annotation that will run complete application context and start serving our endpoints on a random port. Next, we have @Testcontainers annotation which comes from the org.testcontainers.junit.jupiter package and it enables us to use JUnit 5 framework.

The really nice thing with Testcontainers, databases, and R2DBC is that you need only a small change in your application.properties file to run your test database – you need to add tc between driver and database name in the URL to the database and you have to specify image version you want to use.

spring.r2dbc.url=r2dbc:tc:postgresql://localhost:5432/webflux-demo-database?TC_IMAGE_TAG=13.2-alpine

For Testcontainers to pull an image and start a container, you have to have some database operations in your code. To start, we can add a code that we want to execute before each test that will fill a database with some mock data:


@Autowired
lateinit var databaseClient: DatabaseClient

@BeforeEach
fun setUp() {
    // create table on the first run
    databaseClient.execute("""
        CREATE TABLE IF NOT EXISTS languages (
            name text NOT NULL,
            year integer
        );
    """.trimIndent()
    ).then().block()
    // delete all data from previous run
    databaseClient.delete().from("languages").then().block()
    // insert new mock data
    databaseClient.insert()
        .into("languages")
        .value("name", "test 1")
        .then().block()
    databaseClient.insert()
        .into("languages")
        .value("name", "test 2")
        .then().block()
}

Notice usage of block here. Since we are now testing, we need database operations to execute immediately instead of running them when needed as we do with normal Mono or Flux types. Also, batch inserts are not possible with DatabaseClient so we have to execute multiple insert statements. Now, we also need a dummy test:

@Test
fun test() {
    Assertions.assertThat(1).isEqualTo(1)
}

With all this in place, we can run our tests. The first run will take a bit longer (depending on your Internet connection) since it will pull Docker image locally, but the next runs will be faster.

If this works, we can continue with proper testing. For our test, we will need our old friend WebTestClient (the same one we used in controller slice testing):

@Autowired
lateinit var webTestClient: WebTestClient

And now, I have to say that I’ve been lying a bit when I said that we will do integration testing without mocking anything. Calls towards third-party API are something that we can mock since we don’t want our tests to depend on it working. For this blog post, I’ll stick with Mockito and mock our LanguageYearClient interface, but another option would be to use something like MockServer or WireMock. With that being said, here’s our mocked interface:

@MockBean
lateinit var languageYearClient: LanguageYearClient

We now have everything we need to write our integration test that will call an endpoint and verify that it returns data we’ve inserted into a database along with data we will mock in the test itself representing third-party API response. Oh, and we can remove our dummy that that verifies 1 == 1 :)

@Test
@DisplayName("Getting all languages should return 200 with the list of all languages with correct years")
fun getAllLanguagesTest() {
    `when`(languageYearClient.fetchLanguageYear(Language(name = "test 1"))).thenReturn(Mono.just(LanguageYear(2010)))
    `when`(languageYearClient.fetchLanguageYear(Language(name = "test 2"))).thenReturn(Mono.just(LanguageYear(2020)))
    webTestClient
        .get()
        .uri("/languages")
        .exchange()
        .expectStatus().isOk
        .expectBodyList<Language>()
        .contains(*listOf(
            Language(name = "test 1", year = 2010),
            Language(name = "test 2", year = 2020)
        ).toTypedArray())
}

The only weird piece of code here is an argument of the contains method which is a so-called spread operator that converts a list to a variable number of arguments (vararg).

When you run this, the test should be green and, of course, you should try changing mocked years or response structure to make sure tests are then failing. And this is it, our complete integration testing class should now look like this:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class LanguagesIntegrationTest {

    @Autowired
    lateinit var webTestClient: WebTestClient

    @MockBean
    lateinit var languageYearClient: LanguageYearClient

    @Autowired
    lateinit var databaseClient: DatabaseClient

    @BeforeEach
    fun setUp() {
        // create table on the first run
        databaseClient.execute("""
            CREATE TABLE IF NOT EXISTS languages (
                name text NOT NULL,
                year integer
            );
        """.trimIndent()
        ).then().block()
        // delete all data from previous run
        databaseClient.delete().from("languages").then().block()
        // insert new mock data
        databaseClient.insert()
            .into("languages")
            .value("name", "test 1")
            .then().block()
        databaseClient.insert()
            .into("languages")
            .value("name", "test 2")
            .then().block()
    }

    @Test
    @DisplayName("Getting all languages should return 200 with the list of all languages with correct years")
    fun getAllLanguagesTest() {
        `when`(languageYearClient.fetchLanguageYear(Language(name = "test 1"))).thenReturn(Mono.just(LanguageYear(2010)))
        `when`(languageYearClient.fetchLanguageYear(Language(name = "test 2"))).thenReturn(Mono.just(LanguageYear(2020)))
        webTestClient
            .get()
            .uri("/languages")
            .exchange()
            .expectStatus().isOk
            .expectBodyList<Language>()
            .contains(*listOf(
                Language(name = "test 1", year = 2010),
                Language(name = "test 2", year = 2020)
            ).toTypedArray())
    }
}

The final version of the code can be found here. It contains 3 commits that follow steps explained in this blog post starting from refactoring to a testable code, followed by unit tests, and then integration tests at the end.

Conclusion

As I said at the beginning, this blog post wraps up the whole reactive paradigm series as we’ve covered most of the basics need for anybody to start trying out this (no so) new approach to programming. Since this whole series might become a bit outdated as time goes by, I’ll try to update the codebase to reflect any changes and track all changes in some sort of a changelog in the project’s README and I will add a note at the top of the blog post that leads to it.