Writing Test Classes With Kotlin and JUnit 5

After we have finished this webinar, we:

  • Can create test classes with Kotlin.
  • Can add setup, teardown, and test functions to a test class.
  • Know how we can write nested tests with Kotlin and JUnit 5.
  • Understand the invocation order to setup, teardown, and test functions.

Let’s start by creating a test class.

Get the source code from Github

Creating a Test Class

When we create a new test class, we have to create a “normal” Kotlin class. After we have created our test class, its source code looks as follows:

1import org.junit.jupiter.api.DisplayName
2import org.junit.jupiter.api.Tag
3
4@Tag("unitTest")
5@DisplayName("Demonstrates how we can write test classes with Kotlin")
6class TestClassExampleTest {
7
8}

There are two things I want to point out:

  • Even though it’s possible to add multiple classes to one Kotlin file, I think that this isn’t a good idea because we don’t want to write a test suite that consists of long and messy files.
  • When we name our test classes, we should use the suffix: ‘Test’.

Next, we will add setup and teardown functions to our test class.

Adding Setup and Teardown Functions to a Test Class

We can add setup and teardown functions to our test class by following these steps:

First, we have to write a setup function that is run once before any test function is run. We can write this setup function by following these steps:

  1. Add a companion object to our test class.
  2. Add a new setup function to the companion object and implement this function by writing a message to System.out.
  3. Annotate the setup function with the @BeforeAll annotation.
  4. Annotate the setup function with @JvmStatic annotation. This annotation ensures that the Kotlin compiler creates a static method in the enclosing class (TestClassExampleTest) of the object and an instance method in the object itself.

After we have added this setup function to our test class, its source code looks as follows:

 1import org.junit.jupiter.api.BeforeAll
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4
 5@Tag("unitTest")
 6@DisplayName("Demonstrates how we can write test classes with Kotlin")
 7class TestClassExampleTest {
 8
 9    companion object {
10
11        @BeforeAll
12        @JvmStatic
13        fun beforeAll() {
14            println("Before all test functions")
15        }
16    }
17}
It’s not a good idea to write test classes which write information to System.out. I use this technique only because I want to demonstrate the invocation order of setup, teardown, and test functions.

Second, we have to write a teardown function that is run once after all test functions have been run. We can write this teardown function by following these steps:

  1. Add a new teardown function to the companion object and implement this function by writing a message to System.out.
  2. Annotate the teardown function with the @AfterAll and JvmStatic annotations.

After we added this teardown function to our test class, its source code looks as follows:

 1import org.junit.jupiter.api.AfterAll
 2import org.junit.jupiter.api.BeforeAll
 3import org.junit.jupiter.api.DisplayName
 4import org.junit.jupiter.api.Tag
 5
 6@Tag("unitTest")
 7@DisplayName("Demonstrates how we can write test classes with Kotlin")
 8class TestClassExampleTest {
 9
10    companion object {
11
12        @BeforeAll
13        @JvmStatic
14        fun beforeAll() {
15            println("Before all test functions")
16        }
17
18        @AfterAll
19        @JvmStatic
20        fun afterAll() {
21            println("After all test functions")
22        }
23    }
24}

We have to use static methods because we use the default life cycle configuration of JUnit 5. This means that JUnit 5 creates one test object per test method. If we configure JUnit 5 to create one test object per test class, we don’t have to use static methods when we write these setup and teardown functions.

Additional Reading:

Third, we have to write a setup function that is invoked before a test function is run. We can write this setup function by following these steps:

  1. Add new setup function to our test class and implement this function by writing a message to System.out.
  2. Annotate the setup function with the @BeforeEach annotation.

After we have added this setup function to our test class, its source code looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.BeforeEach
 3import org.junit.jupiter.api.DisplayName
 4import org.junit.jupiter.api.Tag
 5import org.junit.jupiter.api.Test
 6import org.junit.jupiter.api.AfterAll
 7import org.junit.jupiter.api.BeforeAll
 8
 9@Tag("unitTest")
10@DisplayName("Demonstrates how we can write test classses with Kotlin")
11class TestClassExampleTest {
12
13    companion object {
14
15        @BeforeAll
16        @JvmStatic
17        fun beforeAll() {
18            println("Before all test functions")
19        }
20
21        @AfterAll
22        @JvmStatic
23        fun afterAll() {
24            println("After all test functions")
25        }
26    }
27
28    @BeforeEach
29    fun beforeEach() {
30        println("Before each test function")
31    }
32}

Fourth, we have to write a teardown function that is invoked after a test function is run. We can write this teardown function by following these steps:

  1. Add new teardown function to our test class and implement this function by writing a message to System.out.
  2. Annotate the teardown function with the @AfterEach annotation.

After we have added this teardown function to our test class, its source code looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.BeforeEach
 3import org.junit.jupiter.api.DisplayName
 4import org.junit.jupiter.api.Tag
 5import org.junit.jupiter.api.Test
 6import org.junit.jupiter.api.AfterAll
 7import org.junit.jupiter.api.AfterEach
 8import org.junit.jupiter.api.BeforeAll
 9
10@Tag("unitTest")
11@DisplayName("Demonstrates how we can write test classses with Kotlin")
12class TestClassExampleTest {
13
14    companion object {
15
16        @BeforeAll
17        @JvmStatic
18        fun beforeAll() {
19            println("Before all test functions")
20        }
21
22        @AfterAll
23        @JvmStatic
24        fun afterAll() {
25            println("After all test functions")
26        }
27    }
28
29    @BeforeEach
30    fun beforeEach() {
31        println("Before each test function")
32    }
33
34    @AfterEach
35    fun afterEach() {
36        println("After each test function")
37    }
38}

We have now added setup and teardown functions to our test class. Let’s move on and find out how we can write nested tests with Kotlin and JUnit 5.

Writing Nested Tests

When we add nested tests to our test class, we have to use inner classes. Let’s add a few inner classes to our test class. The idea of this exercise is to demonstrate the invocation order of setup, teardown, and test functions.

We can add the required inner classes to our test class by following these steps:

First, we have to add a new inner class called A to our test class and annotate the inner class with the @Nested annotation. After we have created the A class, we have to add one setup, teardown, and test function to the created inner class.

 1import org.junit.jupiter.api.BeforeEach
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.api.Test
 5import org.junit.jupiter.api.AfterAll
 6import org.junit.jupiter.api.AfterEach
 7import org.junit.jupiter.api.BeforeAll
 8import org.junit.jupiter.api.Nested
 9
10@Tag("unitTest")
11@DisplayName("Demonstrates how we can write test classses with Kotlin")
12class TestClassExampleTest {
13
14    companion object {
15
16        @BeforeAll
17        @JvmStatic
18        fun beforeAll() {
19            println("Before all test functions")
20        }
21
22        @AfterAll
23        @JvmStatic
24        fun afterAll() {
25            println("After all test functions")
26        }
27    }
28
29    @BeforeEach
30    fun beforeEach() {
31        println("Before each test function")
32    }
33
34    @AfterEach
35    fun afterEach() {
36        println("After each test function")
37    }
38
39    @Nested
40    @DisplayName("Tests for the function A")
41    inner class A {
42
43        @BeforeEach
44        fun beforeEach() {
45            println("Before each test function of the A class")
46        }
47
48        @AfterEach
49        fun afterEach() {
50            println("After each test function of the A class")
51        }
52
53        @Test
54        @DisplayName("Example test for function A")
55        fun testForFunctionA() {
56            println("Example test for function A")
57        }
58    }
59}

Second, we have to add a new inner class called WhenX to the A class and annotate the inner class with the @Nested annotation. After we have created the WhenX class, we have to add one setup, teardown, and test function to the created inner class.

After we have added this inner class to the A class, the source code of our test class looks as follows:

 1import org.junit.jupiter.api.BeforeEach
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.api.Test
 5import org.junit.jupiter.api.AfterAll
 6import org.junit.jupiter.api.AfterEach
 7import org.junit.jupiter.api.BeforeAll
 8import org.junit.jupiter.api.Nested
 9
10@Tag("unitTest")
11@DisplayName("Demonstrates how we can write test classses with Kotlin")
12class TestClassExampleTest {
13
14    companion object {
15
16        @BeforeAll
17        @JvmStatic
18        fun beforeAll() {
19            println("Before all test functions")
20        }
21
22        @AfterAll
23        @JvmStatic
24        fun afterAll() {
25            println("After all test functions")
26        }
27    }
28
29    @BeforeEach
30    fun beforeEach() {
31        println("Before each test function")
32    }
33
34    @AfterEach
35    fun afterEach() {
36        println("After each test function")
37    }
38
39    @Nested
40    @DisplayName("Tests for the function A")
41    inner class A {
42
43        @BeforeEach
44        fun beforeEach() {
45            println("Before each test function of the A class")
46        }
47
48        @AfterEach
49        fun afterEach() {
50            println("After each test function of the A class")
51        }
52
53        @Test
54        @DisplayName("Example test for function A")
55        fun testForFunctionA() {
56            println("Example test for function A")
57        }
58
59        @Nested
60        @DisplayName("When X is true")
61        inner class WhenX {
62
63            @BeforeEach
64            fun beforeEach() {
65                println("Before each test function of the WhenX class")
66            }
67
68            @AfterEach
69            fun afterEach() {
70                println("After each test function of the WhenX class")
71            }
72
73            @Test
74            @DisplayName("Example test for function A when X is true")
75            fun testForFunctionAWhenX() {
76                println("Example test for function A when X is true")
77            }
78        }
79    }
80}

We have now added nested tests to our test class. Next, we will find out what happens when we run our tests.

Running Our Tests

When we run our tests, we should see the following output:

 1Before all test functions
 2Before each test function
 3Before each test function of the A class
 4Example test for function A
 5After each test function of the A class
 6After each test function
 7Before each test function
 8Before each test function of the A class
 9Before each test function of the WhenX class
10Example test for function A when X is true
11After each test function of the WhenX class
12After each test function of the A class
13After each test function
14After all test functions

In other words, JUnit 5 invokes the setup and teardown functions by following the context hierarchy of the invoked test functions. This means that we can eliminate duplicate code by putting our code to the correct place.

We have now written a test class that contains nested setup, teardown, and test functions. Also, we understand the invocation order of these functions. Let’s summarize what we learned from this webinar.

Summary

This webinar has taught us three things:

  • We shouldn’t add multiple test classes to one Kotlin file because this creates a test suite that consists of long and messy files.
  • If we use the default life cycle configuration of JUnit 5 and we have to write setup and teardown functions which are run only once, we have add these functions to a companion object and annotate the functions with the @JvmStatic annotation.
  • When we add nested tests to our test class, we have to use inner classes.

Get the source code from Github