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 }
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.
ReplyDeleteNotice 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.
Nice, thanks, I did not know about constructor injection of dependencies in tests.
Delete