软件测试学习——测试工具学习

前言

这一阵子主要工作集中在 MIT6.824 分布式系统理论方面的学习,集中用 go 语言做了两个大实验,本着一学多用的态度,这次测试工具的学习是 go 语言相关(后续学习 C++测试框架 googleTest)。

golang race 检测工具

什么是 race

多线程(Threads、Goroutine)程序对共享变量变量的修改是复杂的,以n=n+1指令的并行执行为例,如果 t1 和 t2 线程几乎同时取出原始 n 值,在各自线程中完成+1 操作然后储存进变量 n,得到的答案可能并非编程人员想要看到的。

事实上,在上述例子中,我们希望每个线程对共享变量 n 的操作都是有效的,但由于 t1 和 t2 线程几乎同时“看到”n 变量,读取值相同,两次累加从结果看变成了一次累加。

race 的名字变源自于此,这种对共享变量不恰当的操作看起来像像线程间的“赛跑”。 结果是否正确取决于晚些执行到该指令的线程 t2 能否“看到” 先行抵达的线程 t1 的修改(t1 线程执行累加后的变量值)。

race 预防与检测

Go 语言提供的锁机制(mu.lock() mu.unlock())可以帮助我们“锁住”赛跑中的线程,对共享变量加锁是预防 race 出现的常用方式,但是这需要编程人员进行准确细致的编程。不过往往人也有疏忽的时候,可喜的是,golang 在 1.1 之后引入了竞争检测的概念。我们可以使用 go run -race 或者 go build -race 来进行竞争检测。

测试代码 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
)

func main() {
n := 1

circle := 10

for i := 0; i < circle; i++ {
go func() {
temp := n
// time.Sleep(1 * time.Second)
temp *= 10
n = temp
fmt.Println("n is ", n)
}()
}
}

运行指令go run -race,进行 race 检测,如下图

Lab5_1
Lab5_1

发现了 race,但是由于写回操作较快,没有影响最终结果。

取消time.Sleep所在行的注释,再次运行指令go run -race,运行结果如下图

Lab5_2
Lab5_2

发现了 race,且这次由于线程写回前休眠,所有线程都在其余线程写回前读,所以最终结果被影响。

golang goconvey 测试框架

GoConvey 是一个开源的 go 语言测试工具,实际上 go 本身提供了go test测试指令供开发者使用,在目录下使用该指令会对该目录中所有后缀为*_test.go进行测试编译,并对前缀为Test*的函数进行单元检测。

GoConvey 主要是编写并集成好了一些常用的测试语句(如形式、数值断言等),并提供了一个优秀的可视化界面。

GoConvey 的代码仓库

框架安装

由于大陆内部使用 go 语言存在一些限制,而且如果启用GO111MODULE,我需要大幅度调整我的项目格式,所以我没有采用 mod,也没有按照官方建议的go get安装方式。

安装好二进制后,我手动检查了依赖项,并在项目源码\src\github.com分别 clone 几个代码仓库

1
2
3
4
cd $GOPATH/src
git clone https://github.com/smartystreets/goconvey.git ./smartystreets/goconvey
git clone https://github.com/smartystreets/assertions.git ./smartystreets/assertions
git clone https://github.com/jtolds/gls.git ./jtolds/gls

启动 Web 服务

1
2
cd $GOPATH/bin
./goconvey

显示界面如下图,成功。

Lab5_3
Lab5_3

编写测试代码

  • 待检测程序 student.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type Student struct {
Num int
Name string

Chinaese int
English int
Math int
}

func NewStudent(num int, name string) (*Student, error) {
if num < 1 || len(name) < 1 {
return nil, fmt.Errorf("num name empty")
}
stu := new(Student)
stu.Num = num
stu.Name = name
return stu, nil
}

func (this *Student) GetAve() (int, error) {
score := this.Chinaese + this.English + this.Math
if score == 0 {
return 0, fmt.Errorf("score is 0")
}
return score / 3, nil
}

  • 测试程序 student_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"testing"

. "github.com/smartystreets/goconvey/convey"
)

func TestNew(t *testing.T) {
Convey("start test new", t, func() {
stu, err := NewStudent(0, "")
Convey("have error", func() {
So(err, ShouldBeError)
})
Convey("stu is nil", func() {
So(stu, ShouldBeNil)
})
})
}

func TestScore(t *testing.T) {
stu, _ := NewStudent(1, "test")
Convey("if error", t, func() {
_, err := stu.GetAve()
Convey("have error", func() {
So(err, ShouldBeError)
})
})

Convey("normal", t, func() {
stu.Math = 60
stu.Chinaese = 70
stu.English = 80
score, err := stu.GetAve()
Convey("have error", func() {
So(err, ShouldBeError)
})
Convey("score > 60", func() {
So(score, ShouldBeGreaterThan, 60)
})
})
}

执行测试

  • VSCode 单元测试结果(go test 命令行)

    Lab5_4
    Lab5_4

    由于有一个 Error 预期和 nil 实际类型不符合,测试不通过。

  • Web UI

    Lab5_5
    Lab5_5

    同样显示有一个 Error 预期和 nil 实际类型不符合,测试不通过。从结构看,5 个断言,通过 4 个,断言处在哪个测试函数中也清晰明了。极大方便了我们排查缺陷所在。另一个简单程序的测试通过情况如下。

    Lab5_6
    Lab5_6

结语

通过本次阶段性学习,我发现了解、熟悉、熟练使用测试工具是三个层次,过去我只停留在了解层面,将测试部分和业务逻辑部分混在一起编写,排错难,测试烦。

进行了go run -race检测和GoConvey工具的学习后,我才知道原来语言开发者、开源社区还存在着如此高效的工具,不仅减少了 bug 产生的概率,也减少了 bug 发现后修复,回归的时间,可谓一举多得,感谢老师给了我们这个主动学习领域内各种实用知识的机会。