1. 概述

在本教程中,我们将探讨如何对一个基于 Kotlin 的 Spring Boot 应用进行测试。我们会从零开始搭建一个包含 Repository、Service 和 Controller 的简单应用,并分别介绍单元测试和集成测试的常见做法。

目标读者是有一定 Spring Boot 和 Kotlin 使用经验的开发者,因此文中不会对基础概念做过多解释。


2. 项目依赖配置

在开始编码之前,先配置好 Maven 的依赖项。

2.1 Web 与 JPA 依赖

我们需要添加 spring-boot-starter-webspring-boot-starter-data-jpa,以支持 Web 层和持久化层:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.1.4</version>
</dependency>

2.2 嵌入式数据库 H2

为了测试方便,我们使用 H2 数据库作为嵌入式数据库:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    <version>2.0.202</version>
</dependency>

2.3 Kotlin 源码目录配置

确保 Maven 能识别 Kotlin 源文件目录:

<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        <!-- 插件配置见下文 -->
    </plugins>
</build>

2.4 Kotlin 插件配置

为支持 Spring Boot 对 Kotlin 的特性(如无参构造函数、开放类等),我们引入 kotlin-maven-plugin 并启用 all-openno-arg 插件:

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <configuration>
        <args>
            <arg>-Xjsr305=strict</arg>
        </args>
        <compilerPlugins>
            <plugin>spring</plugin>
            <plugin>jpa</plugin>
            <plugin>all-open</plugin>
            <plugin>no-arg</plugin>
        </compilerPlugins>
        <pluginOptions>
            <option>all-open:annotation=javax.persistence.Entity</option>
            <option>all-open:annotation=javax.persistence.Embeddable</option>
            <option>all-open:annotation=javax.persistence.MappedSuperclass</option>
        </pluginOptions>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>1.8.0</version>
        </dependency>
    </dependencies>
</plugin>

3. Spring Boot 应用组件

接下来我们构建一个简单的 Spring Boot 应用,包含 Entity、Repository、Service 和 Controller 四个核心组件。

3.1 实体类:BankAccount

@Entity
data class BankAccount (
    var bankCode: String,
    var accountNumber: String,
    var accountHolderName: String,
    @Id @GeneratedValue var id: Long? = null
)

3.2 Repository:BankAccountRepository

@Repository
interface BankAccountRepository : CrudRepository<BankAccount, Long>

3.3 Service:BankAccountService

@Service
class BankAccountService(var bankAccountRepository: BankAccountRepository) {
    fun addBankAccount(bankAccount: BankAccount): BankAccount {
        return bankAccountRepository.save(bankAccount)
    }

    fun getBankAccount(id: Long): BankAccount? {
        return bankAccountRepository.findByIdOrNull(id)
    }
}

3.4 Controller:BankController

@RestController
@RequestMapping("/api/bankAccount")
class BankController(var bankAccountService: BankAccountService) {

    @PostMapping
    fun addBankAccount(@RequestBody bankAccount: BankAccount): ResponseEntity<BankAccount> {
        return ResponseEntity.ok(bankAccountService.addBankAccount(bankAccount))
    }

    @GetMapping
    fun getBankAccount(@RequestParam id: Long): ResponseEntity<BankAccount> {
        val bankAccount: BankAccount? = bankAccountService.getBankAccount(id)
        return if (bankAccount != null) {
            ResponseEntity.ok(bankAccount)
        } else {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}

4. 测试环境准备

4.1 排除 JUnit Vintage

Spring Boot 默认包含 JUnit 4 的支持,我们使用 JUnit 5,因此需要排除旧版本:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

然后引入 JUnit 5 依赖:

<dependency> 
    <groupId>org.junit.jupiter</groupId> 
    <artifactId>junit-jupiter-engine</artifactId> 
    <version>5.8.1</version> 
    <scope>test</scope> 
</dependency>

4.2 引入 MockK 替代 Mockito

MockK 是 Kotlin 更友好的 mocking 工具:

<dependency>
    <groupId>com.ninja-squad</groupId>
    <artifactId>springmockk</artifactId>
    <version>3.0.1</version>
    <scope>test</scope>
</dependency>

同时排除 Mockito:

<exclusion>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
</exclusion>

5. 单元测试

5.1 使用 MockK 测试 Service

class BankAccountServiceTest {
    val bankAccountRepository: BankAccountRepository = mockk()
    val bankAccountService = BankAccountService(bankAccountRepository)

    @Test
    fun whenGetBankAccount_thenReturnBankAccount() {
        every { bankAccountRepository.findByIdOrNull(1) } returns bankAccount

        val result = bankAccountService.getBankAccount(1)

        verify(exactly = 1) { bankAccountRepository.findByIdOrNull(1) }
        assertEquals(bankAccount, result)
    }
}

5.2 使用 @WebMvcTest 测试 Controller

@WebMvcTest
class BankControllerTest(@Autowired val mockMvc: MockMvc) {

    @MockkBean
    lateinit var bankAccountService: BankAccountService

    @Test
    fun givenExistingBankAccount_whenGetRequest_thenReturnsBankAccountJsonWithStatus200() {
        every { bankAccountService.getBankAccount(1) } returns bankAccount

        mockMvc.perform(get("/api/bankAccount?id=1"))
            .andExpect(status().isOk)
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.bankCode").value("ING"))
    }

    @Test
    fun givenBankAccountDoesntExist_whenGetRequest_thenReturnsStatus400() {
        every { bankAccountService.getBankAccount(2) } returns null

        mockMvc.perform(get("/api/bankAccount?id=2"))
            .andExpect(status().isBadRequest())
    }

    @Test
    fun whenPostRequestWithBankAccountJson_thenReturnsStatus200() {
        every { bankAccountService.addBankAccount(bankAccount) } returns bankAccount

        mockMvc.perform(post("/api/bankAccount")
            .content(mapper.writeValueAsString(bankAccount))
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.bankCode").value("ING"))
    }
}

6. 集成测试

6.1 使用 @DataJpaTest 测试 Repository

@DataJpaTest
class BankAccountRepositoryTest {
    @Autowired
    lateinit var entityManager: TestEntityManager

    @Autowired
    lateinit var bankAccountRepository: BankAccountRepository

    @Test
    fun WhenFindById_thenReturnBankAccount() {
        val ingBankAccount = BankAccount("ING", "123ING456", "JOHN SMITH")
        entityManager.persist(ingBankAccount)
        entityManager.flush()

        val ingBankAccountFound = bankAccountRepository.findByIdOrNull(ingBankAccount.id!!)
        assertThat(ingBankAccountFound == ingBankAccount).isTrue
    }
}

6.2 使用 @SpringBootTest 进行端到端测试

@SpringBootTest(
    classes = [KotlinTestingDemoApplication::class],
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
class KotlinTestingDemoApplicationIntegrationTest {

    @Autowired
    lateinit var restTemplate: TestRestTemplate

    @Test
    fun whenGetCalled_thenShouldBadReqeust() {
        val result = restTemplate.getForEntity("/api/bankAccount?id=2", BankAccount::class.java)

        assertNotNull(result)
        assertEquals(HttpStatus.BAD_REQUEST, result?.statusCode)
    }

    @Test
    fun whePostCalled_thenShouldReturnBankObject() {
        val result = restTemplate.postForEntity(
            "/api/bankAccount",
            BankAccount("ING", "123ING456", "JOHN SMITH"),
            BankAccount::class.java
        )

        assertNotNull(result)
        assertEquals(HttpStatus.OK, result?.statusCode)
        assertEquals("ING", result.body?.bankCode)
    }
}

7. 总结

在本文中,我们构建了一个基于 Kotlin 的 Spring Boot 应用,并演示了以下测试方法:

✅ 使用 MockK 替代 Mockito 进行 Service 层单元测试
✅ 使用 @WebMvcTest 对 Controller 进行隔离测试
✅ 使用 @DataJpaTest 测试 Repository 层
✅ 使用 @SpringBootTest 进行端到端集成测试

这些测试方法覆盖了 Spring Boot 应用的主要组件,适用于大多数项目测试需求。希望对你在使用 Kotlin 编写 Spring Boot 应用时有所帮助。


原始标题:Guide to Spring Boot Testing with Kotlin