PART 1
参考网站Learn Go with tests。
完成该教程中“迭代”章节的练习,理解TDD、重构、测试、基准测试等概念。
在 Go 中 for 用来循环和迭代,Go 语言没有 while,do,until 这几个关键字,只能使用 for。
概念理解
TDD
TDD,测试驱动开发(Test-Driven Development),即测试在先、编码在后的开发实践。TDD是敏捷开发中的一项核心实践和技术,也是一种设计方法论,其原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码,其实施手段是单元测试。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。 总的来说,其核心思想就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。TDD使开发人员对所做的设计或所写的代码有足够的信心,同时也有勇气进行设计或代码的快速重构,有利于快速迭代、持续交付。
在Go语言的工作环境中,TDD可以简单理解为:
对于某个要实现某种功能的函数或库,我们在实现那个函数或库之前,先根据对于某些输入应该产生什么预期结果来编写测试函数。以测试用例驱动功能函数的实现。
TDD的具体实施过程可以分为两个层次:
- 在代码层次,在编码之前写测试代码,可以称为单元测试驱动开发(Unit Test Driven Development,UTDD);
测试代码的作用:在被测代码发生改动后,执行单元测试用例即可验证本次改动是否对函数原有功能造成影响,是未来函数重构的信心保证。 - 在业务层次,在需求分析时就确定需求(如用户故事)的验收标准,即验收测试驱动开发(Acceptance Test Driven Development,ATDD)。
测试
首先先写测试并尝试运行测试:在没有写主要实现的算法之前,先写好测试的文件,测试文件中函数的参数应该是固定的。
然后使用最少的代码来让测试跑起来:为了检测测试代码的编写是否正确,需要先写一个简要的被测试的文件,只需让代码可编译,以检查测试用例能否通过。
最后把代码补全,使得其能够通过测试:若测试文件编写没有问题,则补全被测试文件的代码,补全后,在命令行中输入 go test 命令即可进行测试,或者直接编译运行测试文件,也可以进行测试,其中命令行测试时,加入不同的参数,会测试不同的内容。
重构
一般的文件都有重构的习惯,即假如原有的函数只有两个参数,重构后可以使得参数增加或者减少,然后重新进行测试。重构的前提就是测试就绪(testing is ready),在这样的前提下,重构的风险就很低,否则就有比较高的风险。
基准测试
基准测试(benchmarks)是测试运行代码需要多少的时间,其编写是Go语言的另一个一级特性,与编写测试十分相似。基准测试会有固定的名字,并且有特定的参数,其中 b.N 代表使程序运行多少次,最后在命令行中执行命令 go test -bench=. (不同的操作系统命令可能会有些许区别)。
测试
先写测试
package iteration
import "testing"
func TestRepeat(t *testing.T) {
repeated := Repeat("a")
expected := "aaaaa"
if repeated != expected {
t.Errorf("expected '%q' but got '%q'", expected, repeated)
}
}
尝试运行测试
./repeat_test.go:6:14: undefined: Repeat
先使用最少的代码来让失败的测试先跑起来
现在只需让代码可编译,这样你就可以检查测试用例能否通过。
package iteration
func Repeat(character string) string {
return ""
}
现在已经掌握了足够的 Go 知识来给一些基本的问题编写测试,这意味着可以放心的处理生产环境的代码,并知道它的行为会如你所愿。
repeat_test.go:10: expected 'aaaaa' but got ''
把代码补充完整,使得它能够通过测试
func Repeat(character string) string {
var repeated string
for i := 0; i < 5; i++ {
repeated = repeated + character
}
return repeated
}
与其它语言如 C,Java 或 JavaScript 不同,在 Go 中 for 语句前导条件部分并没有圆括号,而且大括号 { } 是必须的。你可能会好奇下面这行。
var repeated string
我们目前都是使用 := 来声明和初始化变量。然后 := 只是两个步骤的简写。这里我们使用显式的版本来声明一个 string 类型的变量。我们还可以使用 var 来声明函数,稍后我们将看到这一点。
运行测试应该是通过的。
重构
现在是时候重构并引入另一个构造(construct)+= 赋值运算符。
const repeatCount = 5
func Repeat(character string) string {
var repeated string
for i := 0; i < repeatCount; i++ {
repeated += character
}
return repeated
}
+= 是自增赋值运算符(Add AND assignment operator),它把运算符右边的值加到左边并重新赋值给左边。它在其它类型也可以使用,比如整数类型。
基准测试
在 Go 中编写基准测试(benchmarks)是该语言的另一个一级特性,它与编写测试非常相似。
func BenchmarkRepeat(b *testing.B) {
for i := 0; i < b.N; i++ {
Repeat("a")
}
}
你会看到上面的代码和写测试差不多。
testing.B 可使你访问隐性命名(cryptically named)b.N。
基准测试运行时,代码会运行 b.N 次,并测量需要多长时间。
代码运行的次数不会对你产生影响,测试框架会选择一个它所认为的最佳值,以便让你获得更合理的结果。
用 go test -bench=. 来运行基准测试。 (如果在 Windows Powershell 环境下使用 go test -bench=".")
goos: darwin
goarch: amd64
pkg: github.com/quii/learn-go-with-tests/for/v4
10000000 136 ns/op
PASS
以上结果说明运行一次这个函数需要 136 纳秒(在我的电脑上)。这挺不错的,为了测试它运行了 10000000 次。注意:基准测试默认是顺序运行的。
练习
1. 修改测试代码,以便调用者可以指定字符重复的次数,然后修复代码
2. 写一个 ExampleRepeat 来完善你的函数文档
3. 看一下 strings 包。找到你认为可能有用的函数,并对它们编写一些测试。投入时间学习标准库会慢慢得到回报。
PART 2
自己选择一个算法如“快排”,模仿教程内容结构,写一个Go语言某算法实现TDD实践报告。
快排思路
快速排序算法(Quicksort)的主要步骤为:
- 在数组中设定一个分界值,该分界值将数组分成了左右两部分。
- 将大于或等于分界值的数据移动到数组右边,小于分界值的数据移动到数组的左边,从而使左侧部分中的数据都小于或等于分界值,而右侧部分中的数据都大于或等于分界值。
- 左侧和右侧的数据组成的数组可以再同样独立地进行排序:对于左侧数组的数据,又可以取一个分界值,将该数组分成左右两部分,在左侧放置较小值,在右侧放置较大值;而右侧的数组数据也做类似的处理。
- 重复上述过程,产生递归。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也完成了。
Go语言测试
先写测试
最少代码运行测试
代码补全测试
重构
基准测试