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.

Categories
Coding Kotlin Spring Boot

Testing Spring Boot with Kotlintest and Mockk

The code written in this guide is available on Github.

For anyone used to writing behavior tests with Groovy and Spock, they will probably miss how they wrote behavior tests after migrate to Kotlin. Luckily we have a test tool for Kotlin that gives you more or less the same functionality. This tool is called Kotlintest.

This project is created by using Spring Initializr and the project itself uses Kotlin 1.3.60, Java 12 and Spring Boot 2.2.2. Recommended software for writing Kotlin is IntelliJ.

A simple overview of what we are testing

The EntranceService will increment the count for every person that enters, greet the person and add them to a list. When a person leave, they are removed from the list and the counter is decremented.

CounterService

@Component
class CounterService(var count: Int = 0) {

    fun countUp() {
        count += 1
    }

    fun countDown() {
        count -= 1
    }
}

GreeterService

@Component
class GreeterService {

    fun greet(name: String = "there"): String {
        return "Hello $name!"
    }
}

EntranceService

@Component
class EntranceService(
        private val counterService: CounterService,
        private val greeterService: GreeterService
) {
    val people = mutableListOf<Person>()

    fun enter(person: Person) {
        if (!person.anonymous) {
            greeterService.greet(person.name)
        } else {
            greeterService.greet()
        }
        people.add(person)
        counterService.countUp()
    }

    fun exit(person: Person) {
        people.remove(person)
        counterService.countDown()
    }
}

Writing the tests

Setup

We need to add Kotlintest to our project before we write the tests. This is done by including kotlintest-runner-junit5 to our pom.xml.

<dependency>
	<groupId>io.kotlintest</groupId>
	<artifactId>kotlintest-runner-junit5</artifactId>
	<version>3.4.2</version>
	<scope>test</scope>
</dependency>

We also need to add maven-surefire-plugin to our pom.xml.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.22.2</version>
</plugin>

Do NOT include io.kotlintest.kotlintest 2.0.7 as a maven library. This is not necessary for Kotlintest to work, and may actually cause unexpected behavior running your test.

Specs

Different specs is used for structuring the layout of your tests. Full list of specs provided by Kotlintest can be found here.

Greeter Service – StringSpec

Test of the greeting service by use of StringSpec

class GreeterServiceTest : StringSpec({

    "when we have a name, we should be greeted by that name" {
        val name = "Jack"
        val greeter = GreeterService()
        val greeting = greeter.greet(name)
        greeting shouldBe "Hello Jack!"
    }

    "when are anonymous, we should still be greeted" {
        val greeter = GreeterService()
        val greeting = greeter.greet()
        greeting shouldBe "Hello there!"
    }
})

Greeter Service – BehaviorSpec

Another test of the Greeter Service. This time by use of BehaviorSpec and a table with input data.

Note that a table is not necessary for using BehaviorSpec.

class GreeterServiceWithTable : BehaviorSpec() {

    private val greeterService = GreeterService()

    init {
        table(
                headers("name", "expectedResult"),
                row("Jack", "Hello Jack!"),
                row("Jane", "Hello Jane!"),
                row(null, "Hello there!")
        ).forAll { name: String?, expectedResult: String ->
            Given("a person with name $name") {

                When("the person is greeted") {
                    val greeting = if (name != null) {
                        greeterService.greet(name)
                    } else {
                        greeterService.greet()
                    }

                    Then("he is expected to be greeted with: $expectedResult") {
                        greeting shouldContain expectedResult
                    }
                }
            }
        }
    }
}

Counter Service – FunSpec

class CounterServiceTest : FunSpec({

    test("the counter should count from the initial value when the initial value is given") {
        val counter = CounterService(1)
        counter.countDown()
        counter.count shouldBe 0
    }

    test("the counter should count from 0 when no initial value is given") {
        val counter = CounterService()
        counter.countUp()
        counter.count shouldBe 1
    }
})

Entrance Service – BehaviorSpec + Mockk

Mockk is a useful tool for creating mocks in Kotlin and is a nice alternative to Mockito. We start by adding this to the pom.xml.

<dependency>
	<groupId>io.mockk</groupId>
	<artifactId>mockk</artifactId>
	<version>1.9.3</version>
	<scope>test</scope>
</dependency>

Now we can create the test for the Entrance Service

class EntranceServiceTest : BehaviorSpec() {

    private val counterService: CounterService = mockk()
    private val greeterService: GreeterService = mockk()
    private var entranceService = EntranceService(counterService, greeterService)

    override fun beforeTest(testCase: TestCase) {
        super.beforeTest(testCase)

        if (testCase.isTopLevel()) {
            clearAllMocks()
            setupMockks()
        }
    }

    fun setupMockks() {
        every { counterService.countUp() } just Runs
        every { greeterService.greet(any()) } returns "whatever"
    }

    init {
        Given("a known person arrive") {
            val person = Person(name = "Jack", anonymous = false)
            When("the person enters") {
                entranceService.enter(person)
                Then("the service should count the person and greet the person by its name") {
                    verify(exactly = 1) { counterService.countUp() }
                    verify(exactly = 1) { greeterService.greet(person.name) }
                }
            }
        }

        Given("a an anonymous person arrive") {
            val person = Person(name = "Mr. X", anonymous = true)
            When("the person enters") {
                entranceService.enter(person)
                Then("the service should count the person and greet the person") {
                    verify(exactly = 1) { counterService.countUp() }
                    verify(exactly = 1) { greeterService.greet("there") }
                }
            }
        }
    }
}

Note that we have to do clearAllMocks() before each scenario has run or else, verification of exact function calls on the mocks may differ after running several tests. We also need to setup the mocks again after clearing them.

Entrance Service – BehaviorSpec with Spring Boot autowiring

In order to make autowiring to work with BehaviorSpec, there is 3 things you need to have in mind

  • The constructor needs to pass the SpringListener in order to tell Spring Boot that you are running a Spring test. You do that by overriding the listeners function in your test class like this; override fun listeners() = listOf(SpringListener). A more detailed guide can be found here.
  • The autowired items will only appear after the spec has initialized. That means that if you override the beforeSpec, you should not expect the autowired items to be autowired yet. However if you override beforeTest instead, it will work.
  • You need to pull up the Spring context in order for autowiring to work. You do that by annotation your testclass with @SpringBootTest

Before we start writing the test we also need to add an additional dependency in our pom.xml in order to make the Spring Boot autowiring work together with Kotlintest.

<dependency>
	<groupId>io.kotlintest</groupId>
	<artifactId>kotlintest-extensions-spring</artifactId>
	<version>3.4.2</version>
	<scope>test</scope>
</dependency>
@SpringBootTest(classes = [SpringBootKotlintestApplication::class])
class EntranceServiceWithAutowireTest : BehaviorSpec() {

    override fun listeners() = listOf(SpringListener)

    @Autowired
    private lateinit var counterService: CounterService

    @Autowired
    private lateinit var greeterService: GreeterService

    private lateinit var entranceService: EntranceService

    override fun beforeTest(testCase: TestCase) {
        super.beforeTest(testCase)

        if (testCase.isTopLevel()) {
            entranceService = EntranceService(counterService, greeterService)
            reset()
        }
    }

    fun reset() {
        counterService.count = 0
        entranceService.people.clear()
    }

    init {
        Given("a known person arrive") {
            val person = Person(name = "Jack", anonymous = false)
            When("the person enters") {
                entranceService.enter(person)
                Then("the service should count the person and greet the person by its name") {
                    counterService.count shouldBe 1
                }
            }
            When("the person exits") {
                entranceService.exit(person)
                Then("the service should remove the person from the list of counted people") {
                    counterService.count shouldBe 0
                }
            }
        }

        Given("a an anonymous person arrive") {
            val person = Person(name = "Mr. X", anonymous = true)
            When("the person enters") {
                entranceService.enter(person)
                Then("the service should count the person and greet the person") {
                    counterService.count shouldBe 1
                }
            }
            When("the person exits") {
                entranceService.exit(person)
                Then("the service should remove the person from the list of counted people") {
                    counterService.count shouldBe 0
                }
            }
        }
    }
}

I hope this guide was helpful. Feel free to comment or contact me by using the contact form if you have any questions regarding this article 🙂