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 🙂

By Walter

I love coding

Leave a Reply

Your email address will not be published. Required fields are marked *