前言
周末参加了 ThoughtWorks 的线下 TDD 技术交流, 收获还是挺多的, 感觉自己之前一直在闭门造车, 还好是没走太歪. 😅
WHAT
TDD 即 Test Driven Design 测试驱动设计 / Test Driven Development 测试驱动开发. TDD 是极限编程中核心的一项实践.
其中最难理解的就是这个驱动的概念, 至少我个人认为是比较难理解的, 需要实践一段时间才能理解.
WHY
场景一
新需求下来了, 明天上线
为了”快速”上线, 不写测试
写完了, 手工测试一下, 赶紧提测上线
测试人员没测出 Bug, 上线了
结果出 Bug 了
场景二
加新功能了
看着以前的一坨代码, 想重构却又不敢
只好找个角落加个 if else
测试没问题, 提测上线
慢慢就会变成祖传代码
以上两个场景的不足
手工测试
自己认为用 PostMan 测了几次差不多没问题了, 但是没有一个指标去衡量是否测试全面了. 这里就需要 IDE 帮助分析出测试的语句覆盖度, 分支覆盖度, 语句分支都 100% 覆盖了, 才能保证测试全面了, 前提是需要有测试用例.
不要过多依赖测试人员
测试人员只能进行黑盒测试, 测试范围有限.
可能自己检查出这个 Bug 只需要几分钟, 如果直接提交给测试团队的话, 可能需要半天甚至更久, 测试团队才能检查出 Bug, 导致反馈周期变长, 间接导致交付周期变长.
软件变更的成本会随时间与开发阶段增加
能在需求阶段解决的问题就不要拖到开发阶段
能在开发阶段解决的问题就不要拖到测试阶段
重构与测试是相辅相成的
没有测试, 只能提心吊胆的重构
没有重构, 代码会变得越来越混乱
TDD 能解决的痛点
- 薛定谔的代码
- 重构质量无法保证
- 反馈周期长
HOW
在编写实现之前编写测试用例.
一个完整的 TDD 流程
- 需求分解, 拆分为多个 case
- 找产品/客户确认 case 是否正确, 是否有遗漏
- 写测试, 只关注输入输出, 不关注中间过程
- 写实现, 用最简单的实现满足当前的测试
- 重构
- 信心满满的提交代码
TDD Hello World
需求
给出一个 1 到 100 的数字, 如果是 3 的倍数就会得到 “Fizz”, 如果是 5 的倍数就会得到 “Buzz”, 如果即是 3 的倍数也是 5 的倍数就得到 “FizzBuzz”, 如果都不是就会得到原数字.
需求拆解
- 给出 1 得到 1
- 给出 2 得到 2
- 给出 3 得到 Fizz
- 给出 5 得到 Buzz
- 给出 15 得到 FizzBuzz
找产品/客户确认
没通过则修改, 直到通过产品确认后再进行下一步
编写空的实现
只要保证它能够被执行, 不报编译错误即可
public String handle(int input) { return ""; }
编写第一个 case 的测试用例
@Test public void input1() { // GIVEN int input = 1; // WHEN String result = handle(input); // THEN assertThat(result).isEquals("1"); }
编写恰好能够通过此测试用例的实现
public String handle(int input) { return "1"; }
编写第二个 case 的测试用例
@Test public void input2() { // GIVEN int input = 2; // WHEN String result = handle(input); // THEN assertThat(result).isEquals("2"); }
编写恰好能够通过前两个此测试用例的实现
public String handle(int input) { if (input == 1) { return "1"; } else { return "2"; } }
我认为这时候需要重构了, 重构后的代码
public String handle(int input) { return String.valueOf(input); }
跑了一下测试类, 没问题能跑通, 这次重构是成功的
编写第三个 case 的测试用例
@Test
public void input3() {
// GIVEN
int input = 3;
// WHEN
String result = handle(input);
// THEN
assertThat(result).isEquals("Fizz");
}
编写恰好能够通过前三个测试用例的实现
public String handle(int input) {
if (input == 3) {
return "Fizz";
}
return String.valueOf(input);
}
编写第四个 case 的测试用例
@Test
public void input5() {
// GIVEN
int input = 5;
// WHEN
String result = handle(input);
// THEN
assertThat(result).isEquals("Buzz");
}
编写恰好能够通过前四个测试用例的实现
public String handle(int input) {
if (input == 3) {
return "Fizz";
} else if (input == 5) {
return "Buzz";
}
return String.valueOf(input);
}
编写第五个 case 的测试用例
@Test
public void input15() {
// GIVEN
int input = 15;
// WHEN
String result = handle(input);
// THEN
assertThat(result).isEquals("Buzz");
}
编写恰好能够通过前五个测试用例的实现
public String handle(int input) {
if (input == 3) {
return "Fizz";
} else if (input == 5) {
return "Buzz";
} else if (input == 15) {
return "FizzBuzz";
}
return String.valueOf(input);
}
这时候我觉得又该重构了.
重构后的代码
public String handle(int input) {
if (input % 3 == 0 && input % 5 == 0) {
return "FizzBuzz";
} else if (input % 3 == 0) {
return "Fizz";
} else if (input % 5 == 0) {
return "Buzz";
}
return String.valueOf(input);
}
暂且不评论代码本身写的怎么样😅 旨在通过这个例子能够比较好的理解驱动的意义. 由测试类去驱动我们完成代码的编写.