golang|什么才是 TDD 的正确打开方式()

什么才是 TDD 的正确打开方式?
文章目录

  • 什么才是 TDD 的正确打开方式?
    • 一、TDD 的定义
    • 二、为什么用 TDD
      • 1. TDD 的优缺点
      • 2. TDD vs 传统测试
      • 3. TDD 的设计周期
      • 4. TDD 的意义
        • ① 场景驱动
        • ② 自动化
        • ③ 思维方式
    • 三、TDD 使用实例
      • 1. 编程需求
      • 2. 先行测试
        • ① 测试文件编写
        • ② 先行测试的意义
      • 3. 最少代码运行测试
      • 4. 代码补全,进行测试
      • 5. 重构
      • 6. 基准测试
    • 四、使用 TDD 的注意事项
      • ① 不做过多的测试设计
      • ② 小迈步开发
      • ③ 不同时做多件事情
      • ④ 切忌教条
      • ⑤ 使用 TDD 的时机
    • 五、小结

一、TDD 的定义 【golang|什么才是 TDD 的正确打开方式()】golang|什么才是 TDD 的正确打开方式()
文章图片

  • TDD 是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。
  • TDD 的原理是,在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么样的产品代码。
  • 总的来说,TDD 的核心思想就是通过测试来推动整个开发的进行,但并不只是单纯的测试工作,而是要把需求分析,设计,质量控制量化的过程

二、为什么用 TDD 1. TDD 的优缺点
TDD 的思想是从测试样例入手,先写好测试用例,然后再去写实现,它的优缺点也是源自于此
  • 优点:
  1. TDD 又被称为 “测试先行” ,它可以提高产品质量,开发人员一边写测试用例,一边编写业务代码来推动项目,在开发过程中随时可以拿出质量有保证的产品,并不会出现很多 bug
  2. 可以提前了解清楚需求,因为写测试用例的前提是弄懂了需求
  3. 有着快速的反馈,有丰富的测试用例来覆盖业务代码,一旦代码出错,就可以及时发现问题并改正
  • 缺点:
  1. 因为需要对代码进行测试,要写好测试代码,所以无形中增大了开发者要编写的代码量。
  2. 验证不了算法怎么实现,只能判断算法本身的执行结果和性能是否符合要求

2. TDD vs 传统测试
TDD 传统测试
集中在验证测试是否正常工作的生产代码上 更多关注的是测试用例设计
测试每一行代码,实现100%覆盖测试 只测试部分代码,可能遗漏一些测试的case
要求开发者应该 “有目的地测试”。要知道为什么要测试,在测试什么,需要测试什么级别 通常不会考虑可测试的要素,导致代码难以测试
确保开发出来的产品实际上满足它的需求,有助于建立开发者对产品质量的信心 通常是开发完产品才测试,可能会有不少 bug
本质上 TDD 更强调创建生产代码而不是测试设计 专注于测试设计
golang|什么才是 TDD 的正确打开方式()
文章图片


3. TDD 的设计周期
  • 写一个新的测试用例
  • 运行新加的测试用例,看到它失败(还没写功能代码)
  • 编写业务代码,对开发代码做很小的修改,目的就是让新加的测试通过
  • 运行所有的测试用例,然后看到所有测试都通过了
  • 移掉重复的代码,对代码进行重构
    golang|什么才是 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 quicksortimport "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
  • 尝试运行测试,在终端显示失败golang|什么才是 TDD 的正确打开方式()
    文章图片
② 先行测试的意义 看到这里读者可能会产生疑问,既然没有实现代码那测试肯定是错的,为什么还要运行测试呢?原因如下:
  • 思考你应该如何验证你的程序,帮助你写出一种比较容易验证的代码
  • 防止测试本身就是错误的,有可能写出来的测试没有任何意义,无法帮助代码进行重构

3. 最少代码运行测试
  • 为了检测测试代码的编写是否正确,需要先写一个简要的被测试的文件。
  • 先不管任何设计,只要让测试过了就行
  • 在同一个工作目录下的 QuickSort.go 中,实现代码如下
func QuickSort(arr [] int,low,high int) ([]int) { return arr }

  • 在终端运行 go test 后得到以下结果
    golang|什么才是 TDD 的正确打开方式()
    文章图片
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
    golang|什么才是 TDD 的正确打开方式()
    文章图片
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=.
    golang|什么才是 TDD 的正确打开方式()
    文章图片
  • 以上就是 TDD 实现快速排序算法的例子,实际上 TDD 对于排序算法的效果不好,因为排序算法的接口不需要设计。
  • TDD是用来测黑盒的,当需求已经具体到算法上「实现一个快排」TDD 就派不上用场了。我们只是把它作为一个介绍 TDD 流程的例子

四、使用 TDD 的注意事项 ① 不做过多的测试设计
  • 想要在写代码前就作出好的设计是很有难度的。与其在最初去做好的设计,不如不断地重构,使代码进化
  • TDD 希望程序的实现能与现阶段的需求完全契合,不做过度的设计。开发者要做的是根据现在的需求写测试,让测试通过,在此基础上把代码设计的更好更合理。
② 小迈步开发
  • 在 TDD 过程中频繁地运行测试,验证自己的实现或者对业务的理解是否正确。
  • 一直重复 测试失败,测试通过,重构 的循环,步步为营,循序渐进。如果测试出错了,也比较容易回退到之前测试通过的状态。
③ 不同时做多件事情
  • 每个步骤只做这个步骤该做的事情。
  • 在 test failed 时,不要着急把业务逻辑给实现了
  • 在 test passed 里,要做的是让测试尽快通过,而不是一边想着让测试通过,一遍还要重构。结果在测试还没通过的情况下就先重构,结果哪个步骤出错就较难判断。
④ 切忌教条
  • 在使用 TDD 的过程中不要用教条代替自己的思考,比如不要过于追求 100% 代码覆盖率,有时反倒会降低代码质量
  • 此外,100% 代码覆盖率且通过测试的代码未必就代表程序是正确的:因为有可能测试以及实现都错了;或者可能测试仍不完备,存在实现没有测试到的场景
⑤ 使用 TDD 的时机
  • TDD 的特质决定了它在应用开发初期是很缓慢的,只有当产品代码到了一定的规模才能显示出比较明显的优势
  • 此外,TDD 比较追求完美,要求功能代码都能通过测试,导致开发出来的功能有限
  • 众所周知互联网产业是一个争分夺秒的行业,如果企业要求短时间内快速上线一款产品,使用 TDD 可能并不是一个比较好的选择,总的来说需要挑业务

五、小结
  • 本文是对 TDD,即测试驱动开发的正确使用进行讲解。
  • TDD的基本思想是在开发功能代码前,先开发测试代码,并用测试代码验证功能实现是否满足需求或存在缺陷,在测试代码的驱动下优化功能代码的开发
  • TDD 不是银弹,它验证不了算法怎么实现,只能判断算法本身的执行结果和性能是否符合要求。此外它也不可能适合所有的场景,但这不应该成为我们拒绝它的理由。不要轻易否定 TDD,如果要否定,起码要在认真实践过之后。

    推荐阅读