1. 概述
在本教程中,我们将探讨如何对一个基于 Kotlin 的 Spring Boot 应用进行测试。我们会从零开始搭建一个包含 Repository、Service 和 Controller 的简单应用,并分别介绍单元测试和集成测试的常见做法。
目标读者是有一定 Spring Boot 和 Kotlin 使用经验的开发者,因此文中不会对基础概念做过多解释。
2. 项目依赖配置
在开始编码之前,先配置好 Maven 的依赖项。
2.1 Web 与 JPA 依赖
我们需要添加 spring-boot-starter-web
和 spring-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-open
和 no-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 应用时有所帮助。