Categories
Coding Kotlin Spring Boot

Spring Boot Kotlin Guide – Cucumber black-box tests with external services simulated by the use of WireMock

This project is available on GitHub

Introduction

On larger projects with microservice architecture, automated testing is naturally one of the key concerns for test managers and developers. Based on experiences from projects I’ve been working on, and also based on the feedback I have received from other developers on major projects, we’ve come up with a solution that lets us test most of the system when building the project.

The following guide is a real life, simplified example of an implementation of Cucumber tests on a major project.

Project setup

A simple spring boot application can easily be generated based on your liking on https://start.spring.io/.

For this project I chose to go with a Maven project using Java 11, Spring Boot 2 and Kotlin. You should be using IntelliJ for Kotlin. I also chose to add the following plugins for this project:

  • spring-boot-starter-web
  • spring-boot-starter-webflux

Details of necessary plugins for running the Cucumber tests in IntelliJ can be found in the README.md.

Architecture

This is how the application works

InformationController

@RestController
@RequestMapping("/api/information")
class InformationController(private val informationService: InformationService) {

    @PostMapping("/test")
    fun test(@RequestBody information: InformationDto): String {
        return "information was received!"
    }

    @PostMapping("/submit")
    fun submitInformation(@RequestBody information: InformationDto): ResponseDto {
        return informationService.handleInformation(information)
    }
}

This controller has two methods for receiving data with POST

  • The first method, which resolves to path /api/information/test, is for testing integration only. Normally, we would not have such an endpoint on a production ready component, but I kept it here for testing purpose as a part of this guide.
  • The second method, which resolves to path /api/information/submit, takes some information and returns a response after handling the information.

As you can see, both endpoint takes in an object of the type InformationDto:

data class InformationDto(val details: String)

The second endpoint sends the information to a service, informationService, for further handling. After the information has been handled, the controller returns an object of the type ResponseDto:

data class ResponseDto(val message: String) 

You may ask why I’m passing such simple objects instead of strings? This is because I want to prove that serializing and deserializing of objects works perfectly in case someone might want to use this project as a foundation for their application.

In the project files you will also find some simple unit test to make sure that the controller is working. For more information on setting up and testing APIs, see the Spring documentation.

InformationService

@Service
class InformationService(private val informationProcessorClient: InformationProcessorClient) {

    fun handleInformation(informationDto: InformationDto): ResponseDto {
        val processingResult = informationProcessorClient.processInformation(informationDto)

        return ResponseDto(
                message = "Information processed with status=${processingResult.status}"
        )
    }
}

This service simply sends the information further to a client, and after the client has done its job, the InformationService returns the result from the client back to the controller.

InformationProcessorClient

@Component
class InformationProcessorClient(
        @Value("\${external.informationProcessor.url}")
        private val baseUrl: String,
        private val objectMapper: ObjectMapper
) {
    private val client = WebClient.builder()
            .baseUrl(baseUrl)
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build()

    fun processInformation(informationDto: InformationDto): ProcessingResult {
        return client.post().uri("/whatever")
                .bodyValue(objectMapper.writeValueAsString(informationDto))
                .getValidResponse()
                .bodyTo(ProcessingResult::class.java)
    }
}

This class uses WebClient from from the package spring-boot-starter-webflux. The baseUrl is retrieved from our application.yaml in the resources folder.

The value for baseUrl is retrieved from the applicaton.yaml by the use of @Value annotation. You can learn more about @Value here.

When we run test annotated with @ActiveProfiles(“test”), the values from the application-test.yaml will be used as an addition to application.yaml, overwriting any duplicates. So when running tests we will use http://localhost:9001 as baseUrl as you can see from the following excerpts:

Excerpt from application.yaml

external:
  informationProcessor:
    url: ${"dummyValue":INFORMATION_PROCESSOR_URL}

Excerpt from application-test.yaml

external:
  informationProcessor:
    url: "http://localhost:9001"

To make it easier to retrieve the response body from mono, I’ve made a couple of extension functions, getValidResponse and bodyTo.

fun WebClient.RequestHeadersSpec<*>.getValidResponse(): ClientResponse {
    return this.exchange()
            .block() ?: throw NullPointerException("We did not get a valid response from the application!")
}

fun <T : Any> ClientResponse.bodyTo(clazz: Class<T>): T {
    return this.bodyToMono(clazz).block()
            ?: throw RuntimeException("Could not transform response body to ${clazz.simpleName}!")
}

This easy way to extend objects in Kotlin is one of many reasons why I love this programming language over plain Java.

The InformationProcessorClient returns an object of the type ProcessingResult:

data class ProcessingResult(
        val status: String,
        val processed: Boolean
)

Cucumber test setup

The following packages is necessary in order for the test to work. See details in the project pom.xml on GitHub.

  • spring-boot-starter-test (included when creating a Spring Boot project)
  • mockk – powerful mocking tool for Kotlin. Can be compared to Mockito.
  • cucumber-junit
  • cucumber-java8
  • cucumber-spring
  • wiremock – (for simulators)
  • kotlin-logging

RunCucumberTests

@RunWith(Cucumber::class)
@CucumberOptions(
        strict = true,
        glue = ["features"],
        stepNotifications = true,
        features = ["src/test/resources/features"],
        plugin = ["pretty"]
)
class RunCucumberTest

SpringSetup

private val LOGGER = KotlinLogging.logger {}

@ActiveProfiles("test")
@SpringBootTest(
        classes = [Application::class],
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
)
class SpringSetup : En {
    init {
        Before { _ ->
            InformationProcessorSimulator.start()
        }

        After { _ ->
            InformationProcessorSimulator.stop()
        }

        LOGGER.info("Application started")
    }
}

This file makes sure the Spring context is started everytime we run our tests so that we can access the applications API to run our black-box tests. As you can see on the Before and After methods, a simulation of an external service is started before the tests are run, and shut down afterwards. Details regarding this simulator follows.

Simulator

private val LOGGER = KotlinLogging.logger {}

abstract class Simulator(
        private val portNumber: Int,
        private val name: String
) {
    private lateinit var wireMockServer: WireMockServer

    val objectMapper = jacksonObjectMapper()

    lateinit var postMappings: Map<String, (request: Request) -> ResponseDefinition>

    fun start() {
        LOGGER.info("Starting $name on port $portNumber")
        wireMockServer = WireMockServer(options().port(portNumber).extensions(Transformer::class.java))
        mapStubsForPostRequests()
        wireMockServer.start()
    }

    fun stop() {
        LOGGER.info("Stopping $name running on port $portNumber")
        wireMockServer.stop()
    }

    private fun mapStubsForPostRequests() {
        if (::postMappings.isInitialized) {
            for (postMapping in postMappings) {
                wireMockServer.stubFor(
                        WireMock.post(WireMock.urlMatching(postMapping.key))
                                .willReturn(WireMock.aResponse().withTransformer(
                                        Transformer::class.java.simpleName,
                                        "requestHandler", postMapping.value
                                ))
                )
            }
        }
    }
}

This abstract class works as a foundation for creating multiple simulators of different kinds. It takes in a port where you would run this simulator on and a name for logging. It uses WireMock to intercept requests and lets us respond to those requests that fit our criteria. This is done in the function mapStubsForPostRequests where we take in a map where the key/value is as follows:

  • key – a regex string that matches the url we want to intercept
  • value – a function that handles the request
  • The function for handling the response is passed forward to an object of the class Transformer, extending ResponseDefinitionTransformer, since this is the way WireMock wants it:

Transformer

private val LOGGER = KotlinLogging.logger {}

class Transformer : ResponseDefinitionTransformer() {
    override fun getName() = "Transformer"

    override fun transform(
            request: Request?,
            responseDefinition: ResponseDefinition?,
            fileSource: FileSource?,
            parameters: Parameters?
    ): ResponseDefinition {
        require(request != null)
        LOGGER.info(Received request at url=${request.url} with method=${request.method}")

        if (responseDefinition == null || parameters == null) {
            return ResponseDefinitionBuilder().build()
        }

        val requestHandler = parameters["requestHandler"] as (request: Request) -> ResponseDefinition
        return requestHandler(request)
    }
}

This function works as a middleware between our simulator and WireMock. Our function for handling the request is retrieved from the paramters and the request is passed forward to it for further handling.

InformationProcessorSimulator

object InformationProcessorSimulator : Simulator(9001, "InformationProcessorSimulator") {
    init {
        postMappings = mapOf("/whatever" to ::processInformation)
    }

    fun processInformation(request: Request): ResponseDefinition {

        val informationDto: InformationDto = objectMapper.readValue(request.body, InformationDto::class.java)
        val prosessingResult = mockProcessingResult(informationDto)

        return ResponseDefinitionBuilder()
                .withBody(objectMapper.writeValueAsBytes(prosessingResult))
                .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .withStatus(HttpStatus.OK.value())
                .build()
    }

    private fun mockProcessingResult(informationDto: InformationDto): ProcessingResult {
        return if (informationDto.details ==
                "this information should be processed externally and end up with status OK") {
            ProcessingResult(status = "OK", processed = true)
        } else {
            ProcessingResult(status = "FAULTY", processed = true)
        }
    }
}

This simulator listens to requests where the url matches to “/whatever” and it handles the intercepted requests with the function processInformation. This function simply returns a ProcessingResult with the status “OK” or “FAULTY” based on the content of the request body. Note that the simulator is an object. This is to make sure that we have only one instance of it.

There is many things that could improve the simulator, like adding mappings for GET, DELETE and PUT. Support for dynamic ports, and there is most likely better ways to make it more generic. I kept it simple for this guide.

Cucumber test to check integration against our API

#lang: en
Feature: API test with WebClient

  Scenario: The information controller should answer our calls
    Given some information
      | details | whatever |
    When we test the applications API by posting the information
    Then we should receive a response with the text "information was received!"

This test is not really necessary but I added it in order for things to be easier understandable once you peek under the hood.

Cucumber black-box test with WireMock simulator

#lang: en
Feature: Black-box test of our application where external components are simulated with WireMock

  Scenario: Processing of information through application and external services - Information processed with status OK
    Given some information
      | details | this information should be processed externally and end up with status OK |
    When we post the information to the application
    Then we should receive a response informing us that the information processed with status "OK"

  Scenario: Processing of information through application and external services - Information processed with status FAULTY
    Given some information
      | details | this information should be processed externally and end up with status FAULTY |
    When we post the information to the application
    Then we should receive a response informing us that the information processed with status "FAULTY"

This test the following information flow:

InformationFlowStepDefs

class InformationFlowStepDefs : En {

    @Autowired
    private lateinit var objectMapper: ObjectMapper

    private lateinit var information: InformationDto

    private lateinit var response: ClientResponse

    private val client = WebClient.builder()
            .baseUrl("http://localhost:8080/api/information")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build()

    init {
        Given("some information") { dataTable: DataTable ->
            val input = dataTable.asMap<String, String>(String::class.java, String::class.java)

            information = InformationDto(
                    details = input["details"] ?: throw NullPointerException("Missing value for 'details'")
            )
        }

        When("we test the applications API by posting the information") {
            response = client.post().uri("/test")
                    .bodyValue(objectMapper.writeValueAsString(information))
                    .getValidResponse()
        }

        When("we post the information to the application") {
            response = client.post().uri("/submit")
                    .bodyValue(objectMapper.writeValueAsString(information))
                    .getValidResponse()
        }

        Then("we should receive a response with the text {string}") { responseText: String ->
            assert(response.bodyTo(String::class.java) == responseText)
        }

        Then("we should receive a response informing us that the information processed with status {string}") {
            expectedStatus: String ->

            assert(response.statusCode().is2xxSuccessful)
            val responseDto = response.bodyTo(ResponseDto::class.java)
            assert(responseDto.message  == "Information processed with status=$expectedStatus")
        }
    }
}

The code behind the hood of the Cucumber tests. You can download the project from GitHub. If you are new to Kotlin and IntelliJ, note that you might have to mark the kotlin folders under main and test as Sources Root and Test Sources Root. You might also have to install the plugins. See the README.md

I hope you find this guide helpful. Feel free to contact me if you have any questions regarding this guide.