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.
This webinar assumes that:
- You can run Kotlin tests with Maven or Gradle
- You can write test classes with Java and JUnit 5
- You can write nested tests with Java and JUnit 5
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:
- Add a companion object to our test class.
- Add a new setup function to the companion object and implement this function by writing a message to
System.out. - Annotate the setup function with the
@BeforeAllannotation. - Annotate the setup function with
@JvmStaticannotation. 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}
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:
- Add a new teardown function to the companion object and implement this function by writing a message to
System.out. - Annotate the teardown function with the
@AfterAllandJvmStaticannotations.
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:
- Add new setup function to our test class and implement this function by writing a message to
System.out. - Annotate the setup function with the
@BeforeEachannotation.
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:
- Add new teardown function to our test class and implement this function by writing a message to
System.out. - Annotate the teardown function with the
@AfterEachannotation.
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
@JvmStaticannotation. - When we add nested tests to our test class, we have to use inner classes.