社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
本文介绍在 go 语言的基本测试框架下,如何进行测试驱动开发的实现。主要介绍了测试驱动开发的基本概念和测试驱动开发的基本流程。
测试驱动开发:测试驱动开发一种软件开发过程,是敏捷开发中的一项核心技术,也是一种设计方法论。
TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码,将需求转换为非常具体的测试用例,然后对代码进行改进以使测试通过。
重构:代码重构是重组现有过程的程序代码,而不改变其外部行为。
重构旨在改善软件非功能属性的设计,结构或实现,同时保留其功能。重构的潜在优势可能包括提高代码可读性和降低复杂性 ; 这些可以改善源代码可维护性并创建更简单,更简洁或更富表现力的内部架构或对象模型,以提高可扩展性。
测试:描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程,即一种实际输出与预期输出之间的审核或者比较过程。
通常测试会评估系统:
基准测试:运行计算机的一组程序或其他操作,以评估对象的相对性能的行为,通常是通过运行多个标准测试和针对该对象的试验来进行评估。
基准测试旨在模拟组件或系统上的特定类型的工作负载。应用程序基准测试通常可以更好地衡量给定系统的实际性能,从而对应用程序进行相应的改进。
测试驱动开发以测试样例驱动程序的开发,要求在编写某个功能的代码之前先编写测试代码,只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这样的开发方式有助于编写简洁可用和高质量的代码,降低代码的耦合程度,提高程度的内聚度,从而提高整体系统的开发速度。
下面以最长非递减序列为例,介绍在 go 环境下测试驱动开发的基本流程:
测试驱动开发编写功能之前,要先编写对应的测试代码,这样做的目的是明确对应模块或函数的功能和需求,从而提高程序内聚度,降低耦合度。
在该例子中,要求在一段序列中找到最长非递减序列的长度。测试代码如下:
package lis
import "testing"
func TestLis(t *testing.T) {
cases := []struct {
num uint
arr []int
want uint
}{
{7, []int{1, 7, 3, 5, 9, 4, 8}, 4},
{0, []int{}, 0},
}
for _, c := range cases {
got := lis(c.num, c.arr)
if got != c.want {
t.Errorf("lis(%d, %v) == %d, want %d", c.num, c.arr, got, c.want)
}
}
}
该测试检测了基本情况的运行以及相应的边界情况。
在没有编写源代码的时候,测试无法正常进行。但我们也可以尝试运行测试代码,检查测试框架是否被成功导入,测试代码是否存在编译错误。在没有其他问题的情况下,运行测试代码只有一个错误为对应的测试函数未定义。我们切换到对应的工作目录执行 go test
指令,得到如下的效果图:
使用最少的代码让失败的测试先跑起来,这一点在教程中使用斜体字体进行了重点强调。极力强调这一点的原因是,这样的做法可以验证测试工具是否正常工作,且排除了新测试存在缺陷始终通过的可能性。在预期中,该测试就是失败的结果,得到预期的结果,让开发者对测试框架增加信心。
package lis
func lis(num uint, arr []int) uint {
return 0
}
得到的效果如下:
接下来的工作就是完成代码的编写,实现测试的需求,通过相应的测试以及原有的测试。通过测试则说明新代码符合测试要求且不会破坏原有的功能。
上述算法的代码为:
package lis
func lis(num uint, arr []int) uint {
ret := uint(0)
l := make([]uint, num)
if num == 0 {
return 0
}
for j := uint(0); j < num; j++ {
l[j] = 1
for i := uint(0); i < j; i++ {
if arr[i] <= arr[j] && l[i]+1 > l[j] {
l[j] = l[i] + 1
if l[j] > ret {
ret = l[j]
}
}
}
}
return ret
}
完成代码的编写后,可以看到成功完成测试。
以上的算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),我们可以对函数进行重构进一步优化算法,得到时间复杂度为 O ( n log n ) O(nlog n) O(nlogn) 的算法。重构以后的代码为:
package lis
// MAXINT used to init minNum
const MAXINT = 10000
func lis(num uint, arr []int) uint {
if num == 0 {
return 0
}
minNum := make([]int, num)
l := uint(1)
for i := uint(0); i < num; i++ {
minNum[i] = MAXINT
}
minNum[0] = arr[0]
for i := uint(1); i < num; i++ {
loc := binSearch(minNum, 0, l, arr[i])
minNum[loc] = arr[i]
if loc == l {
l++
}
}
return l
}
func binSearch(arr []int, head uint, tail uint, key int) uint {
if head+1 == tail {
if key < arr[head] {
return head
}
return tail
}
mid := uint((head + tail) / 2)
if key < arr[mid] {
return binSearch(arr, head, mid, key)
}
return binSearch(arr, mid, tail, key)
}
重构以后的代码仍然符合测试的要求。达到了提升代码质量,但不改变外部功能的要求。
使用基准测试可以测试程序的运行时间,测试代码如下:
func BenchmarkLis(b *testing.B) {
for i := 0; i < b.N; i++ {
lis(7, []int{1, 7, 3, 5, 9, 4, 1})
}
}
测试的结果如下:
可以看到上述代码运行了 14166468 次,平均每次运行所需时间为 84.9 ns。
依据测试驱动开发,第一步要做的事是重新修改测试代码。修改后的测试代码如下:
func TestRepeat(t *testing.T) {
cases := []struct {
ch string
cnt int
want string
}{
{"a", 5, "aaaaa"},
{"a", 0, ""},
}
for _, c := range cases {
got := Repeat(c.ch, c.cnt)
if got != c.want {
t.Errorf("Repeat(%q, %d) == %q, want %q", c.ch, c.cnt, got, c.want)
}
}
}
未修改源代码前,执行 go test
的效果如下:
修改后的源代码如下:
package iteration
// Repeat for repeatCount times
func Repeat(character string, repeatCount int) string {
var repeated string
for i := 0; i < repeatCount; i++ {
repeated += character
}
return repeated
}
随后执行 go test
指令,验证修改后的代码的正确性:
ExampleRepeat
来完善你的函数文档我们在对应的 iteration 测试函数中增加以下函数:
func ExampleRepeat() {
str := Repeat("b", 7)
fmt.Println(str)
//Output: bbbbbbb
}
执行 go test
可以测试多个测试函数,包括上面的 ExampleRepeat
函数。或者可以执行 go test -v
指令,得到具体的测试结果。
通过这次实验,我们学习了:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!