通过使用 Kotlin / Java 中 Junit5 和 Mockito 测试框架,在预约功能中演示 TDD 开发流程。

TDD 介绍

TDD(Test-Driven Development) 是一种开发流程,中文是「测试驱动开发」。用一句白话形容,就是「先写测试再开发」。先写测试除了能确保测试程式的撰写,还有一个好处:有助于在开发初期厘清程式介面如何设计。详细理论知识可以前往 Wiki 了解,这里不再过多介绍。

TDD 开发流程(5步)

术语说明:

  • 红灯 - Failure - 测试用例失败
  • 绿灯 - Success - 测试用例成功
  • 重构 - Refactor - 重构功能代码

具体步骤:

  1. 选定一个功能,编写测试用例
  2. 执行测试,得到【红灯】
  3. 编写满足测试用例的功能代码
  4. 再次执行,得到【绿灯】
  5. 【重构】代码

小结:

对于每一个功能,在【红灯】-【绿灯】-【重构】间来回循环往复,不断得到完善。

前置工作

代码说明

  • 使用 Kotlin 语言(会有相对应的 Java 代码)
  • 使用到的测试框架
    • Running: JUnit5
    • Mock: MockK / Mockito
    • Assertion: Kotest / AssertJ
  • 只涉及 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 开发流程就此开发完成。

完整代码

🔗 参考链接