Wednesday, March 28, 2018

Kotlin and JUnit 5 @BeforeAll

Introduction

In Kotlin, classes do not have static methods. A Java equivalent semantic can be provided using the concept of a companion object though. This post will go into details of what it takes to support a JUnit 5 @BeforeAll and @AfterAll annotation which depend on the presence of a static methods in test classes.


BeforeAll and AfterAll in Java

Junit 5 @BeforeAll annotated methods are executed before all tests and @AfterAll is exected after all tests. These annotations are expected to be applied to static methods:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Junit5BeforeAllTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(Junit5BeforeAllTest.class);
    
    @BeforeAll
    static void beforeAll() {
        LOGGER.info("beforeAll called");    
    }
    
    @Test
    public void aTest1() {
        LOGGER.info("aTest1 called");
        LOGGER.info(this.toString());        
    }
    
    @Test
    public void aTest2() {
        LOGGER.info("aTest2 called");
        LOGGER.info(this.toString());
    }
    
    @AfterAll
    static void afterAll() {
        LOGGER.info("afterAll called");        
    }
}

A rough flow is - the JUnit platform calls the "@BeforeAll" annotated methods, then for each test it creates an instance of the test class and invokes the test. After all tests are executed, the "@AfterAll" annotated static methods are called, this is borne out by the logs, see how the instance ids(from toString() of Object) is different:

2018-03-28 17:22:03.618  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : beforeAll called
2018-03-28 17:22:03.652  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : aTest1 called
2018-03-28 17:22:03.653  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : com.pivotalservices.cookbook.Junit5BeforeAllTest@7bc1a03d
2018-03-28 17:22:03.663  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : aTest2 called
2018-03-28 17:22:03.664  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : com.pivotalservices.cookbook.Junit5BeforeAllTest@6591f517
2018-03-28 17:22:03.669  INFO   --- [           main] c.p.cookbook.Junit5BeforeAllTest         : afterAll called



This default lifecycle of a JUnit 5 test can be changed by an annotation though if the test class is annotated the following way:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class Junit5BeforeAllTest {
....
}

The advantage now is that the @BeforeAll and @AfterAll annotations can be placed on non-static methods, as the JUnit 5 platform can guarantee that these methods are exactly once before ALL tests. The catch though is that any instance-level state will not be reset before each test.

BeforeAll and AfterAll in Kotlin

So how does this translate to Kotlin -
For the default case of a new test instance per test, an equivalent Kotlin test code looks like this:

import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory

class Junit5BeforeAllKotlinTest {

    @Test
    fun aTest1() {
        LOGGER.info("aTest1 called")
        LOGGER.info(this.toString())
    }

    @Test
    fun aTest2() {
        LOGGER.info("aTest2 called")
        LOGGER.info(this.toString())
    }

    companion object {
        private val LOGGER = LoggerFactory.getLogger(Junit5BeforeAllTest::class.java)


        @BeforeAll
        @JvmStatic
        internal fun beforeAll() {
            LOGGER.info("beforeAll called")
        }

        @AfterAll
        @JvmStatic
        internal fun afterAll() {
            LOGGER.info("afterAll called")
        }
    }
}

A Kotlin companion object with methods annotated with @JvmStatic does the job.

Simpler is the case where the lifecycle is modified:

import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.slf4j.LoggerFactory

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class Junit5BeforeAllKotlinTest {

    private val LOGGER = LoggerFactory.getLogger(Junit5BeforeAllTest::class.java)

    @BeforeAll
    internal fun beforeAll() {
        LOGGER.info("beforeAll called")
    }

    @Test
    fun aTest1() {
        LOGGER.info("aTest1 called")
        LOGGER.info(this.toString())
    }

    @Test
    fun aTest2() {
        LOGGER.info("aTest2 called")
        LOGGER.info(this.toString())
    }


    @AfterAll
    internal fun afterAll() {
        LOGGER.info("afterAll called")
    }
}


My personal preference is for the companion object approach as I like the idea of a deterministic state of the test instance before the test method is executed. Another advantage of the approach is with Spring Boot based tests where you want Spring to act on the test instance (inject dependencies, resolve properties etc) only after @BeforeAll annotated method is called, to make this more concrete consider the following example:

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.junit.jupiter.SpringExtension


@ExtendWith(SpringExtension::class)
@SpringBootTest
class BeforeAllSampleTest {

    @Value("\${some.key}")
    private lateinit var someKey: String

    
    companion object {
        @BeforeAll
        @JvmStatic
        fun beforeClass() {
            System.setProperty("some.key", "some-value")
        }

        @AfterAll
        @JvmStatic
        fun afterClass() {
            System.clearProperty("some.key")
        }
    }

    @Test
    fun testValidateProperties() {
        assertThat(someKey).isEqualTo("some-value")
    }

    @Configuration
    class SpringConfig
}

This kind of a test will not work at all if the lifecycle were changed to "@TestInstance(TestInstance.Lifecycle.PER_CLASS)"

Correction
Per comments from the one and only Sébastien Deleuze, the previous test can be simplified by injecting in dependencies and properties via constructor injection, so the test can be re-written as:

@ExtendWith(SpringExtension::class)
@SpringBootTest
class BeforeAllSampleTest(@Value("\${some.key}") val someKey: String) {
 
    companion object {
        @BeforeAll
        @JvmStatic
        fun beforeClass() {
            System.setProperty("some.key", "some-value")
        }
 
        @AfterAll
        @JvmStatic
        fun afterClass() {
            System.clearProperty("some.key")
        }
    }
 
    @Test
    fun testValidateProperties() {
        assertThat(someKey).isEqualTo("some-value")
    }
 
    @Configuration
    class SpringConfig
}

Reference

This stackoverflow answer was instrumental in my understanding of the nuances of JUnit 5 with Kotlin.

2 comments:

  1. It is possible to define PER_CLASS as the default via a junit-platform.properties file, see Spring Kotlin documentation for more details: https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#testing.

    Notice also that JUnit 5 supports constructor-based injection of beans and properties which allows to deal with val properties and avoid lateinit var which is not idiomatic: https://github.com/sdeleuze/spring-boot-kotlin-demo/blob/956851d7e2922fc718385a1970675eaaa2d3859d/src/test/kotlin/hello/ApplicationTests.kt#L15.

    ReplyDelete
    Replies
    1. Nice, thanks, I did not know about constructor injection of dependencies in tests.

      Delete