Typesafe API testing with Spring Boot, Kotlin and OpenFeign

Typesafe API testing with Spring Boot, Kotlin and OpenFeign

Keep your integration tests as close as possible to your application code, avoid mocking.

Klaus Lehner's photo
Klaus Lehner
·Jan 24, 2022·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • Example application
  • Calling your web controllers in a SpringBootTest
  • Using MockMvc in Spring Boot applications
  • Using TestRestTemplate
  • Using RestAssured
  • Using declarative Feign clients for typesafe API tests
  • Summary and link to the code

In my previous blog entry I've shown you how to do end-to-end testing using Cypress. That means:

  • We take our web application including frontend and deploy it to our target platform with all infrastructure components (database, message broker, cache,...)
  • We execute Cypress scripts towards that deployed application by interacting with the DOM just like the user does via the Browser.

That has the big advantage that we are very close to realistic scenarios just like in production (that's why we call them end-to-end), but it has one major drawback: Usually those tests are being executed after your code changes have been merged to your master branch. I've seen projects where full environments are being provisioned on the target platform on every merge request, but in bigger projects with many team members you most likely can't afford that and it will also take ages for your build pipeline to complete.

That means, E2E-tests - compared to plain unit tests - have the big pain point that feedback comes very late. In many cases too late. That's where you should think about API testing of your backend services as part of the build process. This blog post will show you some approaches to achieve exactly that.

Example application

As our very simple application under test, I chose these few lines of Kotlin code. Everything works with plain Java as well of course, but I love the precision and compactness of Kotlin's syntax, so here we go:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime

@SpringBootApplication
class SpringApiTestingApplication

fun main(args: Array<String>) {
    runApplication<SpringApiTestingApplication>(*args)
}

@RestController
class HelloController {
    @GetMapping("/hello-world")
    fun helloWorld(): Message {
        return Message(message = "Hello World")
    }
}

data class Message(val message: String)

This very simple application will return {message: "Hello World"} on GET /hello-world and that's what we're gonna test now in a couple of ways.

Calling your web controllers in a SpringBootTest

The easiest approach is to just use @SpringBootTest, startup your full application within the JUnit-Test, autowire your controller and call the method on that controller:

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class SpringBootControllerTest(
    @Autowired private val controller: HelloController
) {

    @Test
    fun helloWorld() {
        assertEquals("Hello World", controller.helloWorld().message)
    }
}

This setup is fast and short, it is also fully typesafe, but you are missing a lot here in your web layer:

  • Request Mapping (routing)
  • Object (de)serialization (when using i.e. JSON to transfer data)
  • Servlet filters
  • many more, basically everything from the WebMVC and Security layer

Not the best approach possibly...

Using MockMvc in Spring Boot applications

To get closer to real-life scenarios, Spring provides MockMvc. Without firing up a full-blown web server, it gives you access to almost any HTTP API (GET, POST, HEAD,...) and you also get matchers to check if your controllers return the expected response:

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTest(
    @Autowired private val mockMvc: MockMvc
) {

    @Test
    fun helloWorld() {
        val result = mockMvc.perform(get("/hello-world"))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.message").value("Hello World"))
            .andReturn()

        assertEquals(
           MediaType.APPLICATION_JSON_VALUE, 
           result.response.contentType
        )
    }
}
`

But - and this is also explained in this blog article - be aware of the limitations of that approach:

A mock is a mock!

The problem here is that MockMvc is not the same code that is being executed at runtime. It is quite close, but it is not the same, and there are situations where it behaves differently. Most importantly, you don't do real HTTP requests here over the wire, you don't have full error response handling (when using redirects), and much more.

There is another drawback with MockMvc as well. It requires a lot of redundant code as you have to manually enter all routes and field names (when deserializing JSON) in sync with your application code. When your codebase grows and your API evolves, this can be cumbersome.

Using TestRestTemplate

Also taken from the official Spring docu is that example where we start up the server in a real environment (using WebEnvironment.RANDOM_PORT instead of the default WebEnvironment.MOCK) and then using TestRestTemplate to perform real HTTP requests against our server:

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.web.server.LocalServerPort

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestRestTemplateTest(
    @LocalServerPort private val localServerPort: Int,
    @Autowired private val restTemplate: TestRestTemplate
) {

    @Test
    fun helloWorld() {
        val url = "http://localhost:$localServerPort/hello-world"
        assertThat(restTemplate
            .getForObject(url, Message::class.java).message)
            .isEqualTo("Hello World")
    }
}

Much better already, no mock environment anymore, we are close to a production-like scenario.

Still, we need to manually maintain all routes using TestRestTemplate which is also the case in the next example:

Using RestAssured

RestAssured is a great library to create automated tests against REST APIs. The setup is quite the same as with the TestRestTemplate approach:

import io.restassured.RestAssured
import io.restassured.RestAssured.given
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.http.MediaType

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestAssuredTest(
    @LocalServerPort private val localServerPort: Int
) {

    @BeforeEach
    fun setup() {
        RestAssured.port = localServerPort
    }

    @Test
    fun helloWorld() {
        given().get("/hello-world").then()
            .statusCode(200)
            .assertThat()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body("message", equalTo("Hello World"))
    }

    @Test
    fun helloWorldMapping() {
        assertThat(given().get("/hello-world").`as`(Message::class.java).message)
            .isEqualTo("Hello World")
    }
}

I've added two tests there, one without object mapping and one using Jackson to automatically map to our Message object - so you have similar possibilities as with TestRestTemplate.

Please note that large parts of RestAssured are written in Groovy which means a slightly slower runtime. Anyways, I do prefer the syntax over TestRestTemplate, it makes the code more readable.

Anyways, although we're communicating over HTTP with our server, we still need to manually maintain all routes in our test cases and keep them in sync with the server.

Fortunately, there is a way to get around that as well, and that leads us to the final solution:

Using declarative Feign clients for typesafe API tests

OpenFeign`s declarative REST clients allow us to keep routing and MVC mapping information in one place and reuse all of that in our test cases.

Before we can do that, we need to refactor our server code a bit, and extract an interface out of our HelloController which contains all SpringMVC annotations like @GetMapping, @PostMapping and so on:

interface HelloApi {
    @GetMapping("/hello-world")
    fun helloWorld(): Message
}

@RestController
class HelloController : HelloApi {
    override fun helloWorld(): Message {
        return Message(message = "Hello World")
    }
}

Now, after importing org.springframework.cloud:spring-cloud-starter-openfeign, we can write the following test:

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.cloud.openfeign.FeignClientBuilder
import org.springframework.context.ApplicationContext

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OpenFeignIntegrationTest(
    @LocalServerPort private val localServerPort: Int,
    @Autowired private val applicationContext: ApplicationContext
) {

    private val helloApi =
        FeignTestClientFactory.createClientApi(HelloApi::class.java, localServerPort, applicationContext)

    @Test
    fun helloWorld() {
        assertThat(helloApi.helloWorld().message).isEqualTo("Hello World")
    }
}

object FeignTestClientFactory {
    fun <T> createClientApi(apiClass: Class<T>, port: Int, clientContext: ApplicationContext): T {
        return FeignClientBuilder(clientContext)
            .forType(apiClass, apiClass.canonicalName)
            .url("http://localhost:$port")
            .build()
    }
}

I've extracted a small helper class FeignTestClientFactory here for a more comfortable usage of FeignClientBuilder - you can reuse this utility across your test cases.

The test case itself remains short then:

  • We are again using WebEnvironment.RANDOM_PORT
  • @LocalServerPort is being injected by Spring Boot
  • A declarative Feign clients is created based on our new interface HelloApi. OpenFeign reads our @GetMapping annotations including all routing info and creates an HTTP client for us behind a dynamic proxy of HelloApi
  • That means we can now call all interface methods of HelloApi, but we're not calling our controller directly (like in the first example), instead we are doing real HTTP requests, accessing our server just like any other client would do.

This now brings us a fully type-safe and refactoring-safe API test against our Spring Boot server:

  • There is a single place of definition for routing: HelloApi.
  • If new methods are being added in the API, or existing methods are updated, you have those changes immediately available in your test cases.
  • You have full refactoring support in your IDE.
  • You can also use your IDE to find all tests accessing a certain API (by searching for all usages of HelloApi), and that is especially helpful in big codebases.

There are multiple ways to test your API of Spring Boot applications, and although startup time compared to MockMvc is slightly higher, I prefer the OpenFeign approach.

You can find the whole sample code of all 4 approaches on my Github page.

 
Share this