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 🙂