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 beforeSpec(spec: Spec) {
        super.beforeSpec(spec)
        setupMockks()
    }

    override fun afterTest(testCase: TestCase, result: TestResult) {
        super.afterTest(testCase, result)
        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() after each test 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)

        // Autowired values does not exist before spec
        entranceService = EntranceService(counterService, greeterService)
    }

    override fun afterSpec(spec: Spec) {
        super.afterSpec(spec)

        // Normally we would simply instantiate new objects of these classes, instead of autowiring and manually resetting them
        // because neither CounterService or GreeterService are dependent on the Spring context
        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 🙂

Categories
Coding Kotlin Spring Boot

Testing and verifying Spring Boot events with Cucumber, Mockk and Kotlin

This project is available on GitHub

Introduction

There is many good guides on how to setup a project with spring boot events and how to use them. However, during my time as a developer I could not find any good guides on how to test and verify events in Cucumber, so I decided to make this as my first guide.

Project setup

A simle 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 12, Spring Boot 2 and Kotlin. You should be using IntelliJ for Kotlin.

Architecture

This is how the application works

If you download and spin up this application and take a peek into the console, you’ll see that it starts logging out whenever it publishes an event with message “Hello” and “Howdy” every time the scheduler runs, and you will also see every time the event listener picks up an event that contains the message “Howdy”.

Scheduler

@Component
class Scheduler(val eventPublisher: EventPublisher) {
    var counter = 0

    @Scheduled(fixedDelayString = "\${scheduling.fixedDelayInMilliseconds}")
    fun scheduleSomething() {
        val message = everySecondTimeHowdyOrHello()
        LOGGER.info("Publishing event with message $message")
        eventPublisher.publishEvent(message)
    }

    private fun everySecondTimeHowdyOrHello(): String {
        counter += 1
        return if (counter % 2 == 1) {
            "Hello"
        } else {
            "Howdy"
        }
    }
}

The fixedDelayString is the time between a scheduled run has finished and the next scheduled run is initialized. This value is defined in application.yaml:

scheduling:
  fixedDelayInMilliseconds: 1000

As you can see in this class, it simply publishes a message, “Hello” or “Howdy”, every second time the scheduler is being run.

Note that you need to annotate your application with @EnableScheduling for the scheduler to run.

Event Publisher

This is simply our own custom ApplicationEventPublisher for SoC

@Component
class EventPublisher(var applicationEventPublisher: ApplicationEventPublisher) {

    fun publishEvent(message: String) {
        applicationEventPublisher.publishEvent(
                MyCustomEvent(this, message)
        )
    }
}

Event Listener

This event listener picks up the events that contains the message “Howdy”.

@Component
class EventListener {

    @EventListener(condition = "#myCustomEvent.isHowdy()")
    fun handleEvent(myCustomEvent: MyCustomEvent) {
        LOGGER.info("EventHandler picked up ${myCustomEvent.message}")
    }
}

MyCustomEvent

class MyCustomEvent(source: Any, val message: String) : ApplicationEvent(source) {     fun isHowdy() = (message == "Howdy") }

How to test this with cucumber?

Debugging this application will show us that all of the steps above works perfectly, but how do you verify this in a feature test in Cucumber? The picture below is a small overview on how we are gonna setup our cucumber tests and to verify events published and received.

First we have to add the following libraries to our project

  • mockk
  • cucumber-spring (required for spring autowiring to work)
  • cucumber-junit
  • cucumber-java8
  • kotlintest (optional. Only for assertions)

After that we add a file for running our cucumber tests. This file has to be postfixed Test in order to be picked up by junit.

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

Now we can add our .feature file with the scenarios we want to test

#language:en
Feature: Events

  Scenario: Should publish and listen to events when application starts
    When an event with message "Howdy" is published
    Then our event listener should receive and event with message "Howdy"

After this we have to add spies to the EventPublisher and the EventListener. This is done by overwriting Spring Boots beans of these classes in our TestConfig-file.

We also need a static object to store the valuables we catch from these classes

@TestConfiguration
class TestConfig {

    @Primary
    @Bean
    fun eventPublisherSpy(applicationEventPublisher: ApplicationEventPublisher): EventPublisher {
        val eventPublisher = EventPublisher(applicationEventPublisher)
        val spy = spyk(eventPublisher)
        every { spy.publishEvent(any()) } answers { args ->
            val message = args.invocation.args[0] as String
            SpyStore.publishedEvents.add(message)
            args.invocation.originalCall()
        }
        return spy
    }

    @Primary
    @Bean
    fun eventListenerSpy(): EventListener {
        val eventListener = EventListener()
        val spy = spyk(eventListener)
        every { spy.handleEvent(any()) } answers { args ->
            val event = args.invocation.args[0] as MyCustomEvent
            SpyStore.receivedEvents.add(event.message)
            args.invocation.originalCall()
        }
        return spy
    }
}
object SpyStore {
    val publishedEvents = mutableListOf<String>()
    val receivedEvents = mutableListOf<String>()
}

In order to pull up the application and have the scheduler start, we need to annotate our step definitions file with @SpringBootTest.

In order to make use of our TestConfig to override Spring Boots beans of the classes EventPublisher and EventListener, we need also to annotate our step definitions file with @ContextConfiguration

I prefer a common, base file for having all this configuration so that all our feature files can reuse this instead of configurating this seperately for each spring definitions file. Therefore we create this file called SpringSetup:

@ActiveProfiles("test")
@SpringBootTest(
        classes = [SpringBootCucumberApplication::class],
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
)
@ContextConfiguration(classes = [TestConfig::class])
class SpringSetup {

    @Before
    fun setup() {
        // This one is necessary in order to pull up the spring context
        LOGGER.info("Started the application")
    }
}

Note that @ActiveProfiles(“test”) points to our test-specific configuration file, application-test.yaml

# Configuration for testing
spring:
  main:
    allow-bean-definition-overriding: true

scheduling:
  fixedDelayInMilliseconds: 100

Now when we run our RunCucumberTest we will see the following in the log

Waiting up to 150 milliseconds for event with message=Howdy to be published
When an event with message "Howdy" is published
We have sent 5 messages
We have received 5 messages
 Then our event listener should receive and event with message "Howdy"

And the test should succeed.

I hope this guide has been helpful for you. If you have any questions, feel free to leave a comment 🙂