什么才是 TDD 的正确打开方式?
文章目录
- 什么才是 TDD 的正确打开方式?
-
- 一、TDD 的定义
- 二、为什么用 TDD
-
- 1. TDD 的优缺点
- 2. TDD vs 传统测试
- 3. TDD 的设计周期
- 4. TDD 的意义
-
- 三、TDD 使用实例
-
- 1. 编程需求
- 2. 先行测试
-
- 3. 最少代码运行测试
- 4. 代码补全,进行测试
- 5. 重构
- 6. 基准测试
- 四、使用 TDD 的注意事项
-
- ① 不做过多的测试设计
- ② 小迈步开发
- ③ 不同时做多件事情
- ④ 切忌教条
- ⑤ 使用 TDD 的时机
- 五、小结
一、TDD 的定义
- TDD 是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。
- TDD 的原理是,在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么样的产品代码。
- 总的来说,TDD 的核心思想就是通过测试来推动整个开发的进行,但并不只是单纯的测试工作,而是要把需求分析,设计,质量控制量化的过程
二、为什么用 TDD
1. TDD 的优缺点
TDD 的思想是从测试样例入手,先写好测试用例,然后再去写实现,它的优缺点也是源自于此
- TDD 又被称为 “测试先行” ,它可以提高产品质量,开发人员一边写测试用例,一边编写业务代码来推动项目,在开发过程中随时可以拿出质量有保证的产品,并不会出现很多 bug
- 可以提前了解清楚需求,因为写测试用例的前提是弄懂了需求
- 有着快速的反馈,有丰富的测试用例来覆盖业务代码,一旦代码出错,就可以及时发现问题并改正
- 因为需要对代码进行测试,要写好测试代码,所以无形中增大了开发者要编写的代码量。
- 验证不了算法怎么实现,只能判断算法本身的执行结果和性能是否符合要求
2. TDD vs 传统测试
TDD |
传统测试 |
集中在验证测试是否正常工作的生产代码上 |
更多关注的是测试用例设计 |
测试每一行代码,实现100%覆盖测试 |
只测试部分代码,可能遗漏一些测试的case |
要求开发者应该 “有目的地测试”。要知道为什么要测试,在测试什么,需要测试什么级别 |
通常不会考虑可测试的要素,导致代码难以测试 |
确保开发出来的产品实际上满足它的需求,有助于建立开发者对产品质量的信心 |
通常是开发完产品才测试,可能会有不少 bug |
本质上 TDD 更强调创建生产代码而不是测试设计 |
专注于测试设计 |
3. TDD 的设计周期
- 写一个新的测试用例
- 运行新加的测试用例,看到它失败(还没写功能代码)
- 编写业务代码,对开发代码做很小的修改,目的就是让新加的测试通过
- 运行所有的测试用例,然后看到所有测试都通过了
- 移掉重复的代码,对代码进行重构
4. TDD 的意义
① 场景驱动
- 为了能够实现”先写测试程序,然后编码实现其功能“,开发者必须提前考虑代码要支持哪些场景。需要支持的场景,代码必须实现,不需要支持的场景,代码没必要过度设计。
- TDD(测试驱动开发)直观的解释了,什么样的设计才叫 ”刚刚好“
② 自动化
- 被自动化测试用例覆盖的代码,其修改方式是受保护的,不恰当的修改,会导致测试不过,更早被发现。
- 写出能自动化运行的测试用例,会降低测试成本,在修改代码时也能得到更快的反馈,以更短的时间确定 “自己开发的代码是正确的”
③ 思维方式
- 长期使用 TDD 实践能够改变一个人的编程习惯和思维方式。
- 由于 TDD 追求的是在编写任何实现代码之前,先编写测试用例。当编写代码的人脑里想的是「我要什么」,而不是具体细节的「我要怎么做」的时候,TDD 的目的也就达到了。
三、TDD 使用实例
- TDD 是一个不断调整 Code 的过程,目的是执行通过事先编写的 Test。
因为在 go 中测试的编写十分方便,所以下面笔者将以用 golang
编写快速排序 QuickSort
,来具体展示 TDD 的主要流程
1. 编程需求
- 设计一个
QuickSort
函数,实现快速排序算法。
- 具体要求:给定一个整数序列,通过调用
QuickSort
函数能将其排序为数值从小到大
2. 先行测试
① 测试文件编写
- 在没有写要实现的算法前,先写好测试的文件
- 测试文件要以
XXX_test.go
的格式命名,XXX 是被测试文件的名字。
- 在编写测试函数时,要注意首字母需要大写,且为
TestXXX
的格式,函数的参数固定为 (t *testing.T)
,具体代码如下所示:
package quicksort
import "testing"
func TestQuickSort(t *testing.T) {
arr1 := []int{2, 4, 5, 8, 6, 3, 1, 7}
arr2 := []int{2, 4, 5, 8, 6, 3, 1, 7}
res := QuickSort(arr1, 0, 7)
expect := []int{1, 2, 3, 4, 5, 6, 7, 8}
for i := 0; i < 8; i++ {
if res[i] != expect[i] {
t.Errorf("\n result %v\n but expect %v\n given %v", res, expect, arr2)
break
}
}
}
- Errorf 函数只有当测试失败时才会打印其中的内容,如果成功通过测试会显示 PASS
- 尝试运行测试,在终端显示失败
② 先行测试的意义
看到这里读者可能会产生疑问,既然没有实现代码那测试肯定是错的,为什么还要运行测试呢?原因如下:
- 思考你应该如何验证你的程序,帮助你写出一种比较容易验证的代码
- 防止测试本身就是错误的,有可能写出来的测试没有任何意义,无法帮助代码进行重构
3. 最少代码运行测试
- 为了检测测试代码的编写是否正确,需要先写一个简要的被测试的文件。
- 先不管任何设计,只要让测试过了就行
- 在同一个工作目录下的 QuickSort.go 中,实现代码如下
func QuickSort(arr [] int, low, high int) ([]int) {
return arr
}
- 在终端运行 go test 后得到以下结果
4. 代码补全,进行测试
- 如果测试文件编写没有问题,就可以补全被测试文件的代码,在命令行中输入 go test 命令即可进行测试
- 我们先使用冒泡排序的方式让其通过排序降序测试
func QuickSort(arr [] int, low, high int) ([]int) {
for i := low; i <= high; i++{
for j := i+1; j <= high; j++{
if arr[i] > arr[j] {
arr[i], arr[j] = arr[j], arr[i]
}
}
}
return arr
}
- 在命令行进行测试时,在
go test
后加上不同的参数,会测试不同的内容,具体请参考 Go test 命令行参数
5. 重构
- 快速排序是分而治之思想在排序算法上的应用。本质上说,快速排序应该算是在冒泡排序基础上的递归分治法
- 按照快速排序算法的原理来实现 QuickSort
func partition(arr []int, low, high int) int {
pivot := arr[low]
for low < high {
for low < high && pivot <= arr[high] {
high--
}
arr[low] = arr[high]
for low < high && pivot >= arr[low] {
low++
}
arr[high] = arr[low]
}
arr[low] = pivot
return low
}
func QuickSort(arr [] int, low, high int) ([]int) {
if high > low {
pivot := partition(arr, low, high)
QuickSort(arr, low, pivot-1)
QuickSort(arr, pivot+1, high)
}
return arr
}
- 测试 go test,提示正确,通过 PASS
6. 基准测试
- 基准测试,通常指测试运行代码需要多少的时间
- testing.B 可使你访问隐性命名(cryptically named)b.N,b.N 代表使程序运行多少次
- 基准测试运行时,代码会运行 b.N 次,并测量需要多长时间。
- 代码运行的次数不会对你产生影响,测试框架会选择一个它所认为的最佳值,以便让你获得更合理的结果。
在 QuickSort_test.go 中添加基准测试代码
func BenchmarkQuickSort(b *testing.B) {
arr := []int{2, 4, 5, 8, 6, 3, 1, 7}
for i := 0; i < b.N; i++ {
QuickSort(arr, 0, 7)
}
}
- 命令行中执行命令 go test -bench=.
- 以上就是 TDD 实现快速排序算法的例子,实际上 TDD 对于排序算法的效果不好,因为排序算法的接口不需要设计。
- TDD是用来测黑盒的,当需求已经具体到算法上「实现一个快排」TDD 就派不上用场了。我们只是把它作为一个介绍 TDD 流程的例子
四、使用 TDD 的注意事项
① 不做过多的测试设计
- 想要在写代码前就作出好的设计是很有难度的。与其在最初去做好的设计,不如不断地重构,使代码进化
- TDD 希望程序的实现能与现阶段的需求完全契合,不做过度的设计。开发者要做的是根据现在的需求写测试,让测试通过,在此基础上把代码设计的更好更合理。
② 小迈步开发
- 在 TDD 过程中频繁地运行测试,验证自己的实现或者对业务的理解是否正确。
- 一直重复
测试失败,测试通过,重构
的循环,步步为营,循序渐进。如果测试出错了,也比较容易回退到之前测试通过的状态。
③ 不同时做多件事情
- 每个步骤只做这个步骤该做的事情。
- 在 test failed 时,不要着急把业务逻辑给实现了
- 在 test passed 里,要做的是让测试尽快通过,而不是一边想着让测试通过,一遍还要重构。结果在测试还没通过的情况下就先重构,结果哪个步骤出错就较难判断。
④ 切忌教条
- 在使用 TDD 的过程中不要用教条代替自己的思考,比如不要过于追求 100% 代码覆盖率,有时反倒会降低代码质量
- 此外,100% 代码覆盖率且通过测试的代码未必就代表程序是正确的:因为有可能测试以及实现都错了;或者可能测试仍不完备,存在实现没有测试到的场景
⑤ 使用 TDD 的时机
- TDD 的特质决定了它在应用开发初期是很缓慢的,只有当产品代码到了一定的规模才能显示出比较明显的优势
- 此外,TDD 比较追求完美,要求功能代码都能通过测试,导致开发出来的功能有限
- 众所周知互联网产业是一个争分夺秒的行业,如果企业要求短时间内快速上线一款产品,使用 TDD 可能并不是一个比较好的选择,总的来说需要挑业务
五、小结
- 本文是对 TDD,即测试驱动开发的正确使用进行讲解。
- TDD的基本思想是在开发功能代码前,先开发测试代码,并用测试代码验证功能实现是否满足需求或存在缺陷,在测试代码的驱动下优化功能代码的开发
- TDD 不是银弹,它验证不了算法怎么实现,只能判断算法本身的执行结果和性能是否符合要求。此外它也不可能适合所有的场景,但这不应该成为我们拒绝它的理由。不要轻易否定 TDD,如果要否定,起码要在认真实践过之后。