完成参考教程“迭代”章节的练习,理解TDD
、重构、测试、基准测试等概念。自己选择一个算法如“快排”,模仿教程内容结构,写一个Go语言某算法实现TDD实践报告
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
TDD可以在多个层级上应用,包括单元测试(测试一个类中的代码)、集成测试(测试类之间的交互)、系统测试(测试运行的系统)和系统集成测试(测试运行的系统包括使用的第三方组件)。TDD的实施步骤是:红(失败测试)- 绿(通过测试) – 重构。
在使用TDD开发时,经常会遇到需要被测对象需要依赖其他子系统的情况,但是你希望将测试代码跟依赖项隔离,以保证测试代码仅仅针对当前被测对象或方法展开,这时候你需要的是测试替身。测试替身可以分为四类:
- 虚设替身:只传递但是不会使用到的对象,一般用于填充方法的参数列表。
- 存根替身:总是返回相同的预设响应,其中可能包括一些虚设状态。
- 伪装替身:可以取代真实版本的可用版本(比真实版本还是会差很多)。
- 模拟替身:可以表示一系列期望值的对象,并且可以提供预设响应。
TDD 过程以及步骤的重要性
-
编写一个失败的测试,并查看失败信息,我们知道现在有一个为需求编写的 相关 的测试,并且看到它产生了 易于理解的失败描述。
-
编写最少量的代码使其通过,以获得可以运行的程序。
-
然后 重构,基于我们测试的安全性,以确保我们拥有易于使用的精心编写的代码
测试
将你「领域」内的代码和外界(会引起副作用)分离开会更好。比如fmt.Println
会产生副作用(打印到标准输出),我们发送的字符串在自己的领域内。所以为了更容易测试,我们把这些问题拆分开。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
-
编写测试和函数很类似,其中有一些规则
-
测试程序需要在一个名为
xxx_test.go
的文件中编写 -
测试函数的命名必须以单词
Test
开始 -
注意,你不必在多个测试框架之间进行选择,然后再去理解如何安装它们。你需要的一切都内建在 Go 语言中,语法与你将要编写的其余代码相同。
-
类型 格式 作用 测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确 基准函数 函数名前缀为Benchmark 测试函数的性能 示例函数 函数名前缀为Example 为文档提供示例文档 -
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。基准测试可以用来识别某段代码的 CPU 或者内存效率问题。基准测试可以被用来测试不同的并发模式或者被用来辅助配置工作池的数量,以保证能最大化系统的吞吐量
Go 的基准测试文件名也必须以 _test.go 结尾。同时也必须导入 testing 包。基准测试函数必须以 Bechmark 开头,接收一个指向 testing.B 类型的指针作为唯一参数。为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,所以这里使用了 for 循环。
基准测试框架默认会在持续1秒的时间内,反复调用需要测试的函数。测试框架每次调用测试函数时,都会增加 b.N 的值。第一次调用时,b.N 的值为1。需要注意的是,一定要将所有要进行基准测试的代码放在循环里,并且循环要使用 b.N 的值。否则,测试的结果是不可靠的。
其执行命令为:go test -v -run=“none” -bench=“基准测试函数名”
以下用快排算法进行TDD实践
首先是快排的概念:它以最右边的值s作比较的标准,将整个数列分为三个部份,一个是小于s的部份,一个是大于s的部份,一个是未处理的部份,在排序的过程中,i是小于s的部分,j是大于s的部分,i 与 j 都会不断的往右进行比较与交换,然后将s的值置于中间,接下来就以相同的步骤会左右两边的数列进行排序的动作,整个演算的过程,直接摘录书中的虚拟码来作说明:
QUICKSORT(A, p, r)
if p < r
then q <- PARTITION(A, p, r)
QUICKSORT(A, p, q-1)
QUICKSORT(A, q+1, r)
end QUICKSORT
PARTITION(A, p, r)
x <- A[r]
i <- p-1
for j <- p to r-1
do if A[j] <= x
then i <- i+1
exchange A[i]<->A[j]
exchange A[i+1]<->A[r]
return i+1
end PARTITION
然后先创建测试文件:终端vim QuickSort_test.go 或code 打开vscode 创建测试文件,在这个文件中创建测试函数TestQuickSort():定义好用来测试快排算法的一个例子(一个数组{5,8,9,3,4,2,10,7,1,6})
package QuickSort
import "testing"
const MAXN = 10
var arr = []int{5,8,9,3,4,2,10,7,1,6}
func TestQuickSort(t *testing.T) {
QuickSort(arr, 0, MAXN-1)
expected := []int{1,2,3,4,5,6,7,8,9,10}
flag := true
for i := 0; i < MAXN; i++ {
if arr[i] != expected[i] {
flag = false
}
}
if flag == false {
t.Errorf("expected ")
for i := 0; i < MAXN; i++ {
t.Errorf("%d\t", expected[i])
}
t.Errorf("but got")
for i := 0; i < MAXN; i++ {
t.Errorf("%d\t", arr[i])
}
}
}
使用go test 命令来测试,因为还没创建QuickSort,所以测试编译没有通过。
然后创建快排文件QuickSort.go,用最少代码来完成:
// QuickSort.go
package QuickSort
func QuickSort(arr []int) {
}
这里因为函数的参数与测试文件中函数参数不一样,所以编译失败。
补充好函数参数与测试文件中的一致后,再次执行go test 命令,我们发现编译已经成功,但因为函数不完整测试失败:
完善QuickSort函数:
//QuickSort.go
package QuickSort
import "fmt"
func QuickSort(arr []int, start int, end int)[]int {
if (start<end){
m, n := start, end
base := arr[m]
for {
if (m < n){
for{
if((m < n)&&(arr[n]>=base)){
n--
}else{
arr[m] = arr[n]
break
}
}
for{
if((m < n)&&(arr[m]<=base)){
m++
}else{
arr[n] = arr[m]
break
}
}
}else{
break
}
arr[m] = base
QuickSort(arr, start, m-1)
QuickSort(arr, n+1, end)
}
}
}
我们再次测试,这一次执行go test -v,参数-v用于输出测试的详细信息:
可以看到我们通过了测试。
基准测试
编写基准测试,在QuickSort_test.go文件中加上基准测试代码:
func BenchmarkRepeat(b *testing.B) {
for i := 0; i < b.N; i++ {
QuickSort([]int{5, 3, 4, 2, 1,8, 6, 7, 9,10},0,MAXN-1)
}
}
执行go test -bench=.
命令执行基准测试,可以得到:
重构:
后续可以不断重构QuickSort函数来满足更进一步的要求。比如重构QuickSort函数,取出两个头尾指针参数,变成func QuickSort(s []int),再修改测试文件中函数的参数,再用go test来测试通过。