Go语言:利用 TDD 测试驱动开发帮助理解数组与动态数组(切片)的区别
2023-03-29 更新
经过一段时间的学习与实践,针对 Golang 语言基础的 TDD 驱动开发测试训练已经告一段落,接下来会在此基础上继续使用 TDD 来构建应用程序 。
博主前一部分的代码Github先贴下面👇以供参考
https://github.com/slowlydance2me/My_Golang_Study.git
Array VS Slice
Sum
函数,它使用 for
来循环获取数组中的元素并返回所有元素的总和。先写测试(array)
sum_test.go
中:
package main import "testing" func TestSum(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if want != got { t.Errorf("got %d want %d given, %v", got, want, numbers) } }
数组的容量是我们在声明它时指定的固定值。我们可以通过两种方式初始化数组:
关于其他引用类型的介绍参考👈
在错误信息中打印函数的输入有时很有用。
我们使用 %v
(默认输出格式)占位符来打印输入,它非常适用于展示数组。
运行测试
我们创建一个Sum.go文件,不着急补全,我们先写个框架,让它编译通过
package main func Sum(numbers [5]int) (sum int) { return 0 }
这时测试还会失败,不过会返回明确的错误信息:
sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]
这个时候把代码补充完整,使得它能够通过测试:
func Sum(numbers [5]int) int { sum := 0 for i := 0; i < 5; i++ { sum += numbers[i] } return sum }
可以使用 取下标 也就是 array[index]
语法来获取数组中指定索引对应的值。
在本例中我们使用 for
循环分 5 次取出数组中的元素并与 sum
变量求和。
重构一(改进代码)
我们可以使用 range
语法来让函数变得更加整洁。
func Sum(numbers [5]int) int { sum := 0 for _, number := range numbers { sum += number } return sum }
range
会迭代数组,每次迭代都会返回数组元素的索引和值。我们选择使用 '_'
空白标志符 来忽略索引。
数组和它的类型
[4]int
作为 [5]int
类型的参数传入函数,是不能通过编译的。string
当做 int
类型的参数传入函数一样。Sum
先写测试(slice)
我们会使用 切片类型,它可以接收不同大小的切片集合。语法上和数组非常相似,只是在声明的时候不指定长度:
func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) }
运行测试
当然会编译出错:
同样先使用最少的代码来让失败的测试先跑起来
- 修改现有的 API,将
Sum
函数的参数从数组改为切片。如果这么做我们就有可能会影响使用这个 API 的人,因为我们的 其他 测试不能编译通过。
- 创建一个新函数。
根据目前的情况,并没有人使用我们的函数,所以选择修改原来的函数。
func Sum(numbers []int) int { sum := 0 for _, number := range numbers { sum += number } return sum }
如果你运行测试,它们还是不能编译通过,你必须把之前测试代码中的数组换成切片
再把 Sum
补充完整,使得它能够通过测试:
事实证明,这里需要我们做的只是修复编译器错误,然后测试就通过了。
重构二
我们已经重构了 Sum
函数把参数从数组改为切片。注意不要在重构以后忘记维护你的测试代码。
func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := []int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) }
go test -cover
你会看到:
现在删除一个测试,然后再次运行。
新的想法
这回我们需要一个 SumAll
函数,它接受多个切片,并返回由每个切片元素的总和组成的新切片。
先写测试
func TestSumAll(t *testing.T) { got := SumAll([]int{1,2}, []int{0,9}) want := []int{3, 9} if got != want { t.Errorf("got %v want %v", got, want) } }
运行测试
先使用最少的代码来让失败的测试先跑起来
SumAll
。func SumAll(numbersToSum ...[]int) (sums []int) { return }
这时运行测试会报编译时错误:
./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)
在 Go 中不能对切片使用等号运算符。你可以写一个函数迭代每个元素来检查它们的值。
但是一种比较简单的办法是使用 reflect.DeepEqual
,它在判断两个变量是否相等时十分有用。
func TestSumAll(t *testing.T) { got := SumAll([]int{1,2}, []int{0,9}) want := []int{3, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }
确保你已经在文件头部 import reflect
,这样你才能使用 DeepEqual
方法。
需要注意的是 reflect.DeepEqual
不是「类型安全」的,所以有时候会发生比较怪异的行为。
为了看到这种行为,暂时将测试修改为:
func TestSumAll(t *testing.T) { got := SumAll([]int{1,2}, []int{0,9}) want := "bob" if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }
slice
和 string
。这显然是不合理的,但是却通过了编译!所以使用 reflect.DeepEqual
比较简洁但是在使用时需多加小心。Sum
计算每个参数的总和并把结果放入函数返回的切片中。
func SumAll(numbersToSum ...[]int) (sums []int) { lengthOfNumbers := len(numbersToSum) sums = make([]int, lengthOfNumbers) for i, numbers := range numbersToSum { sums[i] = Sum(numbers) } return }
make
可以在创建切片的时候指定我们需要的长度和容量。=
对切片元素进行赋值。
重构三
mySlice[10]=1
进行赋值,会报运行时错误。append
函数,它能为切片追加一个新值。
func SumAll(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { sums = append(sums, Sum(numbers)) } return sums }
SumAll
变成 SumAllTails
还有一个新想法
先写测试
func TestSumAllTails(t *testing.T) { got := SumAllTails([]int{1,2}, []int{0,9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }
运行测试
./sum_test.go:26:9: undefined: SumAllTails
先使用最少的代码来让失败的测试先跑起来
把函数名称改为 SumAllTails
并重新运行测试
sum_test.go:30: got [3 9] want [2 9]
将代码补充完整使函数能够测试通过
func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { tail := numbers[1:] sums = append(sums, Sum(tail)) } return sums }
我们可以使用语法 slice[low:high]
获取部分切片。如果在冒号的一侧没有数字就会一直取到最边缘的元素。
在我们的函数中,我们使用 numbers[1:]
取到从索引 1 到最后一个元素。
重构四
myEmptySlice[1:]会发生什么?
先写测试
func TestSumAllTails(t *testing.T) { t.Run("make the sums of some slices", func(t *testing.T) { got := SumAllTails([]int{1,2}, []int{0,9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want :=[]int{0, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) }
运行测试
会报 panic
func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { if len(numbers) == 0 { sums = append(sums, 0) } else { tail := numbers[1:] sums = append(sums, Sum(tail)) } } return sums }
重构五
我们的测试代码有一部分是重复的,我们可以把它放到另一个函数中复用。
func TestSumAllTails(t *testing.T) { checkSums := func(t *testing.T, got, want []int) { if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } t.Run("make the sums of tails of", func(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} checkSums(t, got, want) }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want := []int{0, 9} checkSums(t, got, want) }) }
这样使用起来更加方便,而且还能增加代码的类型安全性。如果一个粗心的开发者使用 checkSums(t, got, "dave")
是不能通过编译的
总结
-
数组
-
切片
-
多种方式的切片初始化
-
切片的容量是 固定 的,但是你可以使用
append
从原来的切片中创建一个新切片 -
如何获取部分切片
-
使用
len
获取数组和切片的长度
-
使用测试代码覆盖率的工具
-
reflect.DeepEqual
的妙用和对代码类型安全性的影响
[][]string
本文来自博客园,作者:slowlydance2me,转载请注明原文链接:https://www.cnblogs.com/slowlydance2me/p/17235617.html