Go语言快排实现TDD实践报告
文章目录
实验环境
操作系统:Ubuntu18.04.5LTS-amd64
编辑器:VScode
概念解析
TDD(Test-Driven Development)
TDD过程
- 编写一个失败的测试,并查看失败信息,我们知道现在有一个为需求编写的相关的测试,并且看到它产生了易于理解的失败描述。
- 编写最少量的代码使其通过,以获得可以运行的程序。
- 然后不断重构,基于测试的安全性,以确保我们拥有易于使用的精心编写的代码。
TDD功能
- 便于程序测试,节省调试时间。
- 增强需求分析,加深对用户需求的理解。
- 增强开发与测试的协调沟通。
- 迭代开发,重构后仍可返回上一次能够通过测试的代码。
重构
不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的设计模式和架构,改善其质量、性能,提高其扩展性和维护性,使系统对于需求的变更始终具有较强的适应能力。
TDD与重构有着紧密的联系,在TDD过程中可以看到,代码正是通过不断重构来适应测试,进而满足用户需求的。
单元测试与基准测试
单元测试
单元测试是功能测试,测试各函数的功能是否正常,其还包括覆盖率测试,可视化、量化展示测试的覆盖率(如哪些函数在测试中未涉及等),帮助程序员尽可能测试所有相关的代码。
Go的单元测试框架的要求:
- 文件命名规则:
含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名。 - 函数声明规则:
测试函数的签名必须接收一个指向testing.T类型的指针,并且函数没有返回值。 - 函数命名规则:
单元测试的函数名必须以Test开头,是可导出公开的函数,最好是Test+要测试的方法函数名。
基准测试
基准测试是测试代码性能的方法,主要通过测试CPU和内存等因素,来评估代码性能,帮助程序员提高代码性能。
Go的基准测试框架的要求:
- 文件命名规则:
含有测试代码的go文件以_test.go结尾,Go语言测试工具只认符合这个规则的文件
测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名。 - 函数声明规则:
测试函数的签名必须接收一个指向testing.B类型的指针,并且函数没有返回值。 - 函数命名规则:
单元测试的函数名必须以Benchmark开头,是可导出公开的函数,最好是Benchmark+要测试的方法函数名。 - 函数体设计规则:
b.N 是基准测试框架提供,用于控制循环次数,循环调用测试代码评估性能。
b.ResetTimer()/b.StartTimer()/b.StopTimer()用于控制计时器,准确控制用于性能测试代码的耗时。
“迭代”章节的练习
1. 修改测试代码,以便调用者可以指定字符重复的次数,然后修复代码
原测试代码部分:
package iteration
import "testing"
func TestRepeat(t *testing.T) {
repeated := Repeat("a")
expected := "aaaaa"
if repeated != expected {
t.Errorf("repeated '%q' expected '%q'", repeated, expected)
}
}
func BenchmarkRepeat(b *testing.B) {
for i := 0; i < b.N; i++ {
Repeat("a")
}
}
原代码
package iteration
func Repeat(ch string) string {
var repeated string
for i := 0; i < 5; i++ {
repeated += ch
}
return repeated
}
- 修改测试代码
package iteration
import "testing"
func TestRepeat(t *testing.T) {
repeated := Repeat("a", 5)
expected := "aaaaa"
if repeated != expected {
t.Errorf("repeated '%q' expected '%q'", repeated, expected)
}
}
func BenchmarkRepeat(b *testing.B) {
for i := 0; i < b.N; i++ {
Repeat("a", 5)
}
}
$ go test //测试出错
# github.com/LEEzanhui/iteration
./iteration_test.go:6:20: too many arguments in call to Repeat
have (string, number)
want (string)
FAIL github.com/LEEzanhui/iteration [build failed]
- 修复代码
package iteration
func Repeat(ch string, times int) string {
var repeated string
for i := 0; i < times; i++ {
repeated += ch
}
return repeated
}
$ go test //单元测试成功
PASS
ok github.com/LEEzanhui/iteration 0.001s
$ go test -bench=. //基准测试
goos: linux
goarch: amd64
pkg: github.com/LEEzanhui/iteration
BenchmarkRepeat-8 10000000 126 ns/op
PASS
ok github.com/LEEzanhui/iteration 1.402s
测试成功,代码完成重构;
2. 写一个 ExampleRepeat 来完善你的函数文档
通过查阅文档可知这是一个示例函数,其有格式要求:以Example
开头,如果示例函数包含以 “Output” 开头的行注释,在运行测试时,go 会将示例函数的输出和 “Output” 注释中的值做比较;
在iteration_test.go
文件中,编写一个ExampleRepeat
函数,演示对Repeat
函数的调用:
func ExampleRepeat() {
str1 := "a"
str2 := Repeat(str1, 5)
fmt.Println(str2)
// Output: aaaaa
}
可以运行该函数:
如果运行结果与注释中的Output不同,会报错:
3. 看一下 strings 包。找到你认为可能有用的函数,并对它们编写一些测试。投入时间学习标准库会慢慢得到回报。
访问https://godoc.org/strings可以查看这个包的详细文档,深入了解各个函数。
选择了Count、Index和ToLower三个函数;测试了这些函数的普通情况和一些特殊情况,如空串等,具体可见代码;
package teststringpkg
import (
"strings"
"testing"
)
func TestCount(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got '%d' want '%d'", got, want)
}
}
t.Run("count number", func(t *testing.T) {
got := strings.Count("engineer", "e")
want := 3
assertCorrectMessage(t, got, want)
})
t.Run("substr is empty", func(t *testing.T) {
got := strings.Count("engineer", "")
want := 9
assertCorrectMessage(t, got, want)
})
}
func TestIndex(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got '%d' want '%d'", got, want)
}
}
t.Run("find index", func(t *testing.T) {
got := strings.Index("chicken", "ck")
want := 3
assertCorrectMessage(t, got, want)
})
t.Run("substr not present", func(t *testing.T) {
got := strings.Index("chicken", "chicks")
want := -1
assertCorrectMessage(t, got, want)
})
}
func TestToLower(t *testing.T) {
assertCorrectMessage := func(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got '%q' want '%q'", got, want)
}
}
t.Run("with symbol", func(t *testing.T) {
got := strings.ToLower("chicken_!?")
want := "chicken_!?"
assertCorrectMessage(t, got, want)
})
t.Run("with upper case", func(t *testing.T) {
got := strings.ToLower("GOPher")
want := "gopher"
assertCorrectMessage(t, got, want)
})
}
运行指令:
$ go test github.com/LEEzanhui/teststringpkg -run ^函数名
如:
$ go test github.com/LEEzanhui/teststringpkg -run ^TestCount
运行结果均为:
ok github.com/LEEzanhui/teststringpkg 0.001s
如下图,是测试Index函数:
TDD 应用:快速排序实现
快速排序原理
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序实现
- 创建quicksort.go和quicksort_test.go文件。
略 - 编写quicksort_test.go
编写时为了方便比较计算结果,准备用ArrayCompare
函数来完成比较,并对该函数也写了测试函数;
package quicksort
import "testing"
func TestQuickSort(t *testing.T) {
cases := []struct {
in, want []int
}{
{[]int{4, 2, 7, 10, 6, 1, 3}, []int{1, 2, 3, 4, 6, 7, 10}},
{[]int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}, []int{0, 1, 1, 3, 4, 5, 6, 7, 8, 9}},
}
for _, c := range cases {
got := QuickSort(c.in, len(c.in))
if !ArrayCompare(got, c.want) {
t.Errorf("Quicksort(%d) == %d, want %d", c.in, got, c.want)
}
}
}
func TestArrayCompare(t *testing.T) {
cases := []struct {
in1, in2 []int
want bool
}{
{[]int{4, 2, 7, 10, 6, 1, 3}, []int{1, 2, 3, 4, 6, 7, 10}, false},
{[]int{1, 2, 3}, []int{1, 2, 3}, true},
{[]int{1, 2, 3}, []int{1, 2}, false},
}
for _, c := range cases {
got := ArrayCompare(c.in1, c.in2)
if got != c.want {
t.Errorf("ArrayCompare(%d, %d) == %t, want %t", c.in1, c.in2, got, c.want)
}
}
}
显然测试函数无法运行,因为QuickSort和ArrayCompare都未定义;
- 先使用最少的代码来让失败的测试先跑起来
package quicksort
// QuickSort quicksort
func QuickSort(in []int, length int) []int {
out := make([]int, length)
return out
}
// ArrayCompare : retun true if two int array is same
func ArrayCompare(arr1 []int, arr2 []int) bool {
return true
}
显然无法通过测试:
- 把代码补充完整,使得它能够通过测试
过程中涉及一定的重构,就不逐步展示了;
package quicksort
// QuickSort quicksort
func QuickSort(in []int, length int) []int {
out := make([]int, length)
for i := 0; i < length; i++ {
out[i] = in[i]
}
QSRecur(&out, 0, len(out)-1)
return out
}
// QSRecur recursive called by QuickSort
func QSRecur(in *[]int, l, r int) {
if l < r {
i, j, baseNum := l, r, (*in)[l]
for i < j {
for i < j && (*in)[j] >= baseNum {
j--
}
if i < j {
(*in)[i] = (*in)[j]
i++
}
for i < j && (*in)[i] < baseNum {
i++
}
if i < j {
(*in)[j] = (*in)[i]
j--
}
}
(*in)[i] = baseNum
QSRecur(in, l, i-1)
QSRecur(in, i+1, r)
}
}
// ArrayCompare : retun true if two int array is same
func ArrayCompare(arr1 []int, arr2 []int) bool {
if len(arr1) != len(arr2) {
return false
}
for i := 0; i < len(arr1); i++ {
if arr1[i] != arr2[i] {
return false
}
}
return true
}
-
测试
除直接用指令启动测试外,vscode也提供了按键实现一键测试,一般在测试文件的开头或函数上方;
选择文件开头的run package tests
,会执行该文件下的所有测试函数,并附带部分参数的结果,见下图:
可见其不但测试了结果,而且还考虑了运行时间timeout并显示了测试覆盖率,上图显示了测试覆盖了100%的语句; -
修改quicksort_test.go,加入基准测试
func BenchmarkQuickSort(b *testing.B) {
for i := 0; i < b.N; i++ {
arr := []int{7, 8, 5, 4, 1, 0, 2, 9, 3, 6}
QuickSort(arr, len(arr))
}
}
测试指令
$ go test -bench=.
结果:
总结
本次实验的主要目标是了解TDD、测试等概念,并进行实践,对于Go语言这门测试驱动开发的语言,该技能是基础;此外还进一步了解了Go的语法知识,如数组和切片等,能够在编程中进行运用。