Preface

Usually we write Spring projects using Java language for business development and Java for unit testing. But Java is not very efficient in writing test code due to its lengthy code, and we usually consider multiple scenarios when writing the test code, so the amount of code expands dramatically, which brings a lot of time wastage. The biggest headache is the MockMvc mock request test, Java does not support multi-line strings until 15, which leads to the need to splice line by line, which is very unintuitive to read and does not make good use of the Intellij IDEA injection language.

So what can we do to solve these problems?

Thinking

We know that Java runs on top of the Java Virtual Machine (JVM), which is inherently language-agnostic, and can run on the JVM regardless of the language the upper layer is written in, as long as it can be compiled into a bytecode file. So the JVM is not just a program that can run Java. For example, Kotlin, Scala, and Groovy can all run on the JVM. In addition to running different code, languages can also call each other because the code is compiled and then has no relation to the higher-level language (we are all bytecode, so why can’t we work together 🤣).

So in this way, we can write test code in a more intuitive and convenient language. Kotlin and Groovy have good compatibility with Spring, and we can use both languages to write test code (this article uses Kotlin).

Implementation

Creating a project

First, we need to create a project (of course, you can also modify on the existing project), the creation of the same way as the pure Java project can be created.

Configure Maven

We need to configure the Maven dependencies and plugins, i.e. the pom.xml file, according to Kotlin’s documentation.

First modify the properties property, the same as Spring was created with a java.version, we need to add a kotlin.version property. Separating out the versions will help with subsequent maintenance.

1
2
3
4
<properties>
  <java.version>11</java.version>
  <kotlin.version>1.5.0</kotlin.version>
</properties>

Then add the dependencies for the Kotlin standard library. Note that we only need to use Kotlin in our test environment, so define scope as test .

1
2
3
4
5
6
<dependency>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-stdlib</artifactId>
  <version>${kotlin.version}</version>
  <scope>test</scope>
</dependency>

Next is to configure the Maven plugin for compiling Kotlin code, copy the following code directly into the build.plugins tag. Be careful not to delete Spring’s Maven.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<plugin>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-maven-plugin</artifactId>
  <version>${kotlin.version}</version>
  <executions>
    <execution>
      <id>compile</id>
      <goals>
        <goal>compile</goal>
      </goals>
      <configuration>
        <sourceDirs>
          <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
          <sourceDir>${project.basedir}/src/main/java</sourceDir>
        </sourceDirs>
      </configuration>
    </execution>
    <execution>
      <id>test-compile</id>
      <goals>
        <goal>test-compile</goal>
      </goals>
      <configuration>
        <sourceDirs>
          <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
          <sourceDir>${project.basedir}/src/test/java</sourceDir>
        </sourceDirs>
      </configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.5.1</version>
  <executions>
    <!-- Replacing default-compile as it is treated specially by maven -->
    <execution>
      <id>default-compile</id>
      <phase>none</phase>
    </execution>
    <!-- Replacing default-testCompile as it is treated specially by maven -->
    <execution>
      <id>default-testCompile</id>
      <phase>none</phase>
    </execution>
    <execution>
      <id>java-compile</id>
      <phase>compile</phase>
      <goals>
        <goal>compile</goal>
      </goals>
    </execution>
    <execution>
      <id>java-test-compile</id>
      <phase>test-compile</phase>
      <goals>
        <goal>testCompile</goal>
      </goals>
    </execution>
  </executions>
</plugin>

At this point, the project has the ability to compile Kotlin. However, we have not imported any Kotlin test libraries yet, so it is not convenient to use JUnit only, so we can add some assertion libraries for writing tests easily.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependency>
  <groupId>io.kotest</groupId>
  <artifactId>kotest-runner-junit5-jvm</artifactId>
  <version>4.5.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.kotest</groupId>
  <artifactId>kotest-assertions-core-jvm</artifactId>
  <version>4.5.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.kotest</groupId>
  <artifactId>kotest-assertions-json</artifactId>
  <version>4.5.0</version>
  <scope>test</scope>
</dependency>

Kotest is also a unit testing tool similar to JUnit, but we will only use the assertion feature, because Kotest has Spring testing support, but you may encounter a lot of strange problems (at least I didn’t get it right, I might as well just use JUnit).

Writing business

With the test environment configured, we can start writing the business code. This article does not use the actual project for testing, just write a random controller and a few functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestController
public class IndexController {

    @GetMapping("/index")
    public String get() {
        return "index";
    }

    @PostMapping("/index")
    public String post(@RequestParam("value") final String value) {
        return value;
    }

    @PutMapping("/json")
    public JsonNode json(@RequestBody JsonNode node) {
        return node;
    }
}
1
2
3
4
5
6
7
@Service
public class UserService {

    public String getUserName(final Long id) {
        return "username: " + id;
    }
}

Writing tests

We need to create a kotlin folder in the test folder to store the Kotlin test code (you can write it directly in the java folder, but it’s better to standardize it), and then set kotlin as the test folder (IDEA will not recognize it automatically).

idea kotlin

The code used to test the UserService is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootTest
class UserServiceTest {
    @Autowired
    private lateinit var userService: UserService

    @Test
    fun getUserName() {
        userService.getUserName(1) shouldBe "username: 1"
        userService.getUserName(2) should {
            it shouldBe "username: 2"
            it.length shouldBe 11
        }
    }
}

You can see that Kotlin provides a lot of syntactic sugar to avoid writing unintuitive and repetitive code when testing. Java’s code is a bit less intuitive in comparison.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void getUserName() {
        assertEquals("username: 1", userService.getUserName(1L));
        final String userName = userService.getUserName(2L);
        assertEquals("username: 2", userName);
        assertEquals(11, userName.length());
    }
}

MockMvc testing

Java is fine for normal unit test code, but the advantages of Kotlin come into play in MockMvc testing. Spring provides a lot of DSL support for Kotlin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@WebMvcTest(IndexController::class)
class MockMvcTest {
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun get() {
        mockMvc.get("/index").andExpect {
            status { isOk() }
            content {
                contentTypeCompatibleWith("text/plain")
                string("index")
            }
        }
    }

    @Test
    fun post() {
        mockMvc.post("/index") {
            param("value", "test value")
        }.andExpect {
            status { isOk() }
            content {
                string("test value")
            }
        }.andDo {
            print()
            handle {
                println(it.response.characterEncoding)
            }
        }
    }

    @Test
    fun json() {
        mockMvc.put("/json") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = """
                {
                  "key": "value",
                  "key2": {
                    "key3": [1, 2, 3]
                  }
                }
            """.trimIndent()
        }.andExpect {
            status { isOk() }
            content {
                contentType(MediaType.APPLICATION_JSON)
            }
            jsonPath("$.key") {
                value("value")
            }
            jsonPath("$.key2.key3.length()") {
                value(3)
            }
        }
    }
}

Reference https://blog.ixk.me/post/writing-spring-tests-with-kotlin