3、第一个TDD实践:需求分析→写测试→写实现→重构,以计算器为例走通完整TDD流程
好,咱们终于到了动手环节。
前面两章讲了TDD的理念和工具链,说实话,光听不练容易飘。这一章咱们就真刀真枪干一把——用计算器这个经典例子,把TDD的完整流程走一遍。
你可能会想:“计算器?太简单了吧?”
嗯,我当年也这么想。但后来发现,越是简单的例子,越能让你把注意力放在流程本身,而不是被复杂的业务逻辑带偏。说白了,咱们这章的目标不是写一个多牛的计算器,而是让你彻底搞懂“红-绿-重构”这三个步骤到底怎么玩。
3.1 需求分析:别急着写代码,先想清楚要什么
我个人习惯,拿到需求第一件事不是打开IDE,而是拿张纸或者开个备忘录,把需求一条条列出来。
咱们这个计算器,需求其实很明确:
- 支持加法、减法、乘法、除法
- 输入两个数字,返回计算结果
- 除法要考虑除数为0的情况
- 结果要精确,不能有浮点数精度问题
你看,就这么几条。但别小看它,每一条背后都有坑。
比如“结果要精确”——用Float还是Double?用BigDecimal?我刚开始做项目时,就因为在计算器里用了Double,结果0.1+0.2等于0.30000000000000004,被测试小姐姐追着骂了一下午。
好,需求明确了。接下来咱们进入TDD的核心环节。
3.2 写测试:先让测试失败(红)
TDD的第一条铁律:没有测试之前,绝不写实现代码。
你可能会觉得别扭:“我连代码都没写,怎么测?”
别急,咱们先写一个最简单的测试——加法。
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
import java.math.BigDecimal
class CalculatorTest {
@Test
fun `should return 4 when adding 2 and 2`() {
// 给定
val calculator = Calculator()
// 当
val result = calculator.add(BigDecimal("2"), BigDecimal("2"))
// 那么
assertEquals(BigDecimal("4"), result)
}
}
写完之后,你猜会发生什么?
对,编译失败。因为Calculator类根本不存在。
这就是“红”阶段——测试失败,而且失败得明明白白。IDE会告诉你:找不到Calculator类,找不到add方法。
3.3 写实现:让测试通过(绿)
好,现在测试是红的。咱们的目标只有一个:让这个测试变绿。
注意,是“让测试变绿”,不是“写出完美的代码”。
所以,最简单的实现是什么?
import java.math.BigDecimal
class Calculator {
fun add(a: BigDecimal, b: BigDecimal): BigDecimal {
return a.add(b)
}
}
就这么简单。运行测试——绿了。
你可能会觉得:“这也太敷衍了吧?”
嗯,没错。TDD的精髓就是:用最少的代码让测试通过。别想着一步到位,后面还有重构环节呢。
接下来,咱们用同样的方式,把减法、乘法、除法的测试和实现都补上。
// 减法测试
@Test
fun `should return 2 when subtracting 2 from 4`() {
val calculator = Calculator()
val result = calculator.subtract(BigDecimal("4"), BigDecimal("2"))
assertEquals(BigDecimal("2"), result)
}
// 乘法测试
@Test
fun `should return 6 when multiplying 2 and 3`() {
val calculator = Calculator()
val result = calculator.multiply(BigDecimal("2"), BigDecimal("3"))
assertEquals(BigDecimal("6"), result)
}
// 除法测试
@Test
fun `should return 2 when dividing 6 by 3`() {
val calculator = Calculator()
val result = calculator.divide(BigDecimal("6"), BigDecimal("3"))
assertEquals(BigDecimal("2"), result)
}
对应的实现:
fun subtract(a: BigDecimal, b: BigDecimal): BigDecimal = a.subtract(b)
fun multiply(a: BigDecimal, b: BigDecimal): BigDecimal = a.multiply(b)
fun divide(a: BigDecimal, b: BigDecimal): BigDecimal = a.divide(b, 10, RoundingMode.HALF_UP)
3.4 处理边界情况:除数为0
好,基本功能都绿了。但咱们的需求里还有一条:除数为0的情况。
按照TDD的节奏,先写测试:
@Test
fun `should throw exception when dividing by zero`() {
val calculator = Calculator()
assertThrows<IllegalArgumentException> {
calculator.divide(BigDecimal("6"), BigDecimal("0"))
}
}
运行测试——红。因为咱们的divide方法还没处理除数为0的情况。
然后改实现:
fun divide(a: BigDecimal, b: BigDecimal): BigDecimal {
require(b != BigDecimal.ZERO) { "除数不能为0" }
return a.divide(b, 10, RoundingMode.HALF_UP)
}
再跑测试——绿了。
3.5 重构:让代码更干净
现在所有测试都是绿的。但你看咱们的Calculator类,每个方法都是独立的,没什么问题。
不过,如果以后要加更多功能(比如开方、幂运算),这个类会越来越臃肿。所以,咱们可以做一个简单的重构:把运算逻辑拆出来。
// 重构后的Calculator
class Calculator(private val operation: Operation) {
fun execute(a: BigDecimal, b: BigDecimal): BigDecimal {
return operation.apply(a, b)
}
}
// 策略模式
interface Operation {
fun apply(a: BigDecimal, b: BigDecimal): BigDecimal
}
class Add : Operation {
override fun apply(a: BigDecimal, b: BigDecimal): BigDecimal = a.add(b)
}
class Subtract : Operation {
override fun apply(a: BigDecimal, b: BigDecimal): BigDecimal = a.subtract(b)
}
class Multiply : Operation {
override fun apply(a: BigDecimal, b: BigDecimal): BigDecimal = a.multiply(b)
}
class Divide : Operation {
override fun apply(a: BigDecimal, b: BigDecimal): BigDecimal {
require(b != BigDecimal.ZERO) { "除数不能为0" }
return a.divide(b, 10, RoundingMode.HALF_UP)
}
}
重构完,记得跑一遍测试——全部绿。完美。
3.6 总结:TDD的节奏感
好了,咱们走完了第一个完整的TDD流程。来,回顾一下:
| 步骤 | 做什么 | 状态 |
|---|---|---|
| 1. 需求分析 | 列出功能点、边界情况 | — |
| 2. 写测试 | 先写一个测试,编译失败 | 红 |
| 3. 写实现 | 用最少的代码让测试通过 | 绿 |
| 4. 重构 | 优化代码结构,保持测试通过 | 绿 |
说白了,TDD就是让你在“红-绿-重构”这个循环里反复横跳。每次只加一点点功能,每次都有测试兜底。
我刚开始练TDD时,觉得这个节奏太慢了。但后来发现,正是这种“慢”,让我少加了多少个通宵班。你想想看,如果每次改代码都有测试帮你验证,你还怕什么?
下一章,咱们会把这个计算器项目放到真实项目中,看看怎么用TDD驱动一个完整的Android功能开发。到时候,你会看到TDD在复杂场景下的真正威力。