通过使用 Kotlin / Java 中 Junit5 和 Mockito 测试框架,在预约功能中演示 TDD 开发流程。
TDD 介绍
TDD(Test-Driven Development) 是一种开发流程,中文是「测试驱动开发」。用一句白话形容,就是「先写测试再开发」。先写测试除了能确保测试程式的撰写,还有一个好处:有助于在开发初期厘清程式介面如何设计。详细理论知识可以前往 Wiki 了解,这里不再过多介绍。
TDD 开发流程(5步)
术语说明:
- 红灯 - Failure - 测试用例失败
- 绿灯 - Success - 测试用例成功
- 重构 - Refactor - 重构功能代码
具体步骤:
- 选定一个功能,编写测试用例
- 执行测试,得到【红灯】
- 编写满足测试用例的功能代码
- 再次执行,得到【绿灯】
- 【重构】代码
小结:
对于每一个功能,在【红灯】-【绿灯】-【重构】间来回循环往复,不断得到完善。
前置工作
代码说明
- 使用 Kotlin 语言(会有相对应的 Java 代码)
- 使用到的测试框架
- Running:
JUnit5
- Mock:
MockK
/Mockito
- Assertion:
Kotest
/AssertJ
- Running:
- 只涉及 TDD 的具体流程,不涉及单元测试如何编写(可以看 SpringBoot 单元测试各层)
功能介绍
假设一个用户预约的场景。
- 用户可以创建一个预约
- 同一个时间点,只有一个用户可以下单成功
使用到的库
kotlin
plugins {
java
id("io.freefair.lombok") version "6.0.0-m2"
kotlin("jvm") version "1.5.0-RC"
}
group = "io.github.lexcao"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
// for Java mocking and assertion
testImplementation("org.mockito:mockito-core:3.9.0")
testImplementation("org.assertj:assertj-core:3.19.0")
// for Kotlin mocking and assertion
testImplementation("io.mockk:mockk:1.11.0")
testImplementation("io.kotest:kotest-assertions-core:4.4.3")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
tasks {
test {
useJUnitPlatform()
}
}
ReservationService
前置
提前创建以下空文件,避免代码无法运行
- Reservation.java
- ReservationService.java
- ReservationRepository.java
Red - 01 - 编写单元测试
执行单元测试,显示【红灯】,代码分支:
- kotlin-red-01
- java-red-01
kotlin
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ReservationServiceImplTest {
private val service: ReservationService = ReservationServiceImpl()
@Nested
inner class MakeReservation {
private val time: LocalDateTime = LocalDateTime.of(2021, 5, 1, 21, 30)
@Test
fun shouldSuccess() {
// given
val reservation = Reservation(name = "Tom", time = time)
// actual
val reserved: Reservation = service.makeReservation(reservation)
// expect
reserved shouldBe reservation
}
}
}
Green - 01 - 编写实现
执行单元测试,显示【绿灯】,代码分支:
- kotlin-green-01
- java-green-01
kotlin
class ReservationServiceImpl : ReservationService {
override fun makeReservation(reservation: Reservation): Reservation {
return reservation
}
}
Red - 02 - 加入功能 - 完善单元测试
(注意:持久化层目前不需要关心,在这里使用 mock 相关功能)
加入持久化逻辑,完善代码,显示【红灯】,代码分支:
- kotlin-red-02
- java-red-02
kotlin
private val mockRepository: ReservationRepository = mockk()
private val service: ReservationService = ReservationServiceImpl(mockRepository)
@AfterEach
fun clear() {
clearAllMocks()
}
@Test
fun shouldSuccess() {
// given
every { mockRepository.save(any()) } returns reservation
// verify
verifySequence {
mockRepository.save(reservation)
}
// ...
}
Green - 02 - 编写实现 - 完善功能
代码分支:
- kotlin-green-02
- java-green-02
kotlin
override fun makeReservation(reservation: Reservation): Reservation {
return repository.save(reservation)
}
Red - 03 - 边界测试
当同一时间内已有预约的情况下,代码分支:
- kotlin-red-03
- java-red-03
kotlin
@Test
fun shouldSuccess() {
// given ...
every { mockRepository.findByTime(time) } returns null
// verify ...
mockRepository.findByTime(time)
}
@Test
fun shouldFailure() {
// given
val reservation = Reservation(name = "Tom", time = time)
every { mockRepository.findByTime(time) } returns reservation
// actual
shouldThrow<ReservationTimeNotAvailable> {
service.makeReservation(reservation)
}
// verify
verifySequence {
mockRepository.findByTime(time)
mockRepository.save(reservation) wasNot Called
}
}
Green - 03 - 完善边界检查
kotlin
override fun makeReservation(reservation: Reservation): Reservation {
val
mayBeReserved = repository.findByTime(reservation.time)
if (mayBeReserved != null) {
throw ReservationTimeNotAvailable
}
return repository.save(reservation)
}
Refactor - 简单的小重构
别忘了,重构完之后,运行一遍单元测试,【绿灯】。代码分支:
- kotlin-refactor
override fun makeReservation(reservation: Reservation): Reservation {
repository.findByTime(reservation.time)?.run {
throw ReservationTimeNotAvailable
}
return repository.save(reservation)
}
小结
一个简单的小功能通过 TDD 开发流程就此开发完成。