Go 单元测试从 0 到 1

2021-07-23 16:09:12 浏览数 (1)

1.什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小被测功能模块。

在 Go 中,一般指对函数的单元测试。

2.单元测试的作用

单元测试可以检查我们的代码能否按照预期执行,来提升代码质量。

通过单元测试,我们可以设置多个测试用例,执行要测试的函数,判断是否符合预期。尽可能达保证函数功能没有问题,或者出现我们预知的错误。一次书写测试用例,随着代码一起永久保留,来验证函数功能,这就是单元测试的好处。

3.Go 如何写单元测试

Go 本身对自动化测试非常友好,并且有许多优秀的测试框架支持,非常好上手。

首先看一下 Go 官方的 testing 包。

要编写一个测试文件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数。将该文件放在与被测试文件相同的包中,该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。

测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值,函数名必须以 Test 开头,建议后跟要测试的函数名。

记得一定要先看一下 Package testing 的官方文档!

下面利用 Go 官方 testing 包给出一个示例。

代码目录结构:

代码语言:javascript复制
gotest
	- go.mod
	- go.sum
	- main.go
	- hello
		- hello.go
		- hello_test.go

main.go:

代码语言:javascript复制
package main

import (
	"main/hello"
)

func main() {
	fmt.Println(hello.Hello())
}

hello.go:

代码语言:javascript复制
package hello

func Hello() string {
	return "Hello world"
}

我们新建一个单测文件 hello_test.go,为函数 Hello() 添加单元测试。

hello_test.go:

代码语言:javascript复制
package hello

import "testing"

func TestHello(t *testing.T) {
	got := Hello()
	want := "Hello world"
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

进入 hello 目录执行命令go test,或指定目录 go test ./hello,所有以_test.go结尾的源码文件内以 Test 开头的函数会自动被执行。

输出:

代码语言:javascript复制
PASS
ok      main/hello      0.173s

我们也可以加上-v选项,输出单测执行的详细过程:

代码语言:javascript复制
go test -v
=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS
ok      main/hello      0.176s

该结果,表示单测通过了,返回的值与我们预期的值是相同的。

现在尝试把预期结果修改一下:

代码语言:javascript复制
want := "Hello fuck"

测试结果:

代码语言:javascript复制
D:codegotesthello>go test -v
=== RUN   TestHello
    hello_test.go:9: got "Hello world" want "Hello fuck"
--- FAIL: TestHello (0.00s)
FAIL
exit status 1
FAIL    main/hello      0.127s

此时提示测试不通过,得到的值与预期的值不相同。

至此,利用 Go 官方包 testing 便完成了一个简单的单元测试,我们可以进行正确或错误的测试。

4.go test 命令参数

go test 是 Go 用来执行单元测试地命令,常用的选项有:

代码语言:javascript复制
-bench regexp 执行相应的 benchmarks,例如 -bench=.;
-cover 开启测试覆盖率;
-run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
-v 显示测试的详细命令。

详见官方文档Testing flags。

现在再添加一个被测试函数。

代码语言:javascript复制
func Add(a, b int) int {
	return a   b
}

测试代码:

代码语言:javascript复制
func TestAdd(t *testing.T) {
	sum := Add(5, 5)
	if sum == 10 {
		t.Log("the result is ok")
	} else {
		t.Fatal("the result is wrong")
	}
}

使用 -run 来测试,发现只运行了 TestAdd 测试方法。

代码语言:javascript复制
D:codegotesthello>go test -v -run TestAdd
=== RUN   TestAdd
    hello_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
ok      main/hello      0.170s

5.快速生成单测代码

实际上,不同函数的单测代码虽然逻辑不同,但结构是一样的,长得非常相似,因此重复的代码可以使用工具来生成,不用手动繁琐地重复书写。

常用的 IDE,比如 GoLand 或 VSCode,都自带了生成单元测试代码的工具,以 GoLand 为例,可以快速为函数、文件或包生成测试代码。在源码文件中”右键函数名 > Generate… > Test for function“ 便可以快速生成对应函数的单测代码模板,然后我们在生成的模板代码中添加具体的测试用例即可。

使用该方法,为上面的 Hello() 函数生成的测试代码如下:

代码语言:javascript复制
func TestHello(t *testing.T) {
	tests := []struct {
		name string
		want string
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Hello(); got != tt.want {
				t.Errorf("Hello() = %v, want %v", got, tt.want)
			}
		})
	}
}

我们在注释处添加测试用例即可,非常快捷方便。

6.看看单元测试覆盖率

写好测试后,可以利用 Go 自带的工具 test coverage 查看一下单元测试覆盖率。

测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码得到执行。 如果执行测试函数导致 80%的语句得到了运行,则测试覆盖率为 80%。

我们来试一下。

代码语言:javascript复制
D:codegotest>go test -v -cover ./hello
=== RUN   TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestAdd
    hello_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
coverage: 100.0% of statements
ok      main/hello      0.154s  coverage: 100.0% of statements

可以看到,目录 hello 下的所有单测都通过了,且报告覆盖率为 100%.

7.使用单测框架写单测

学会使用 Go 官方 testing 包写单元测试是远远不够的,因为实际项目开发中,面对复杂的逻辑判断,繁多的测试用例,网络IO调用等,都加大了单测编写与管理的难度,此时我们需要用到更好的测试框架来增强测试编写。

这里推荐使用 Testify Gomonkey 开源库来完成 Go 的单元测试的书写。

Testify 主要提供测试套件和断言的能力,不过也提供了 mock 的功能,但我们不使用,因为有更好用的 mock 库。

Gomonkey 主要用于提供 mock 能力。

说到 mock,其本意是模拟,就是对一些不想执行的函数,比如有网络IO或对DB有写入的函数,因为测试环境网络不通或不想执行单测而向DB写入数据,都可以将其 mock 住,写一个替代函数。执行单测的时候会调用这个替代函数,相当于替代函数模拟了原函数。

下面使用 Testify Gomonkey 给出使用示例。

先改造一下 Hello() 和 Add() 函数。

代码语言:javascript复制
// Hello 返回 Hello world 和写入 DB 的值
func Hello(a, b int) string {
	return fmt.Sprintf("Hello world and %v", Add(a, b))
}

// Add 将 a b 和写入 DB 并返回
func Add(a, b int) int {
	sum := a   b
	// 省略的 DB 操作
	return sum
}

我们为 Hello() 函数添加单元测试并 mock 住 Add() 函数。

代码语言:javascript复制
package hello

import (
	"testing"

	"github.com/agiledragon/gomonkey"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
)

// TestSuiteHello 测试套件
type TestSuiteHello struct {
	suite.Suite
	patches []*gomonkey.Patches
}

// SetupTest 测试套件初始化
func (suite *TestSuiteHello) SetupTest() {
	// mock Add 函数
	p := gomonkey.ApplyFunc(Add, func(a, b int) int {
		// 不执行DB写入,只返回加和结果
		return a   b
	})
	suite.patches = append(suite.patches, p)
}

// TearDownSuite 测试套件析构
func (suite *TestSuiteHello) TearDownSuite() {
	for _, p := range suite.patches {
		p.Reset()
	}
}

// TestHelloSuite 启动测试套件
func TestHelloSuite(t *testing.T) {
	suite.Run(t, new(TestSuiteHello))
}

// TestHello hello 函数单测
func (suite *TestSuiteHello) TestHello() {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{"testcase1", args{1, 2}, "Hello world and 3"},
		{"testcase2", args{3, 4}, "Hello world and 7"},
		{"testcase3", args{5, 6}, "Hello world and 11"},
	}
	for _, t := range tests {
		res := Hello(t.args.a, t.args.b)
		assert.Equal(suite.T(), t.want, res, t.name)
	}
}

自定义的测试套件 TestSuiteHello,可以添加多个以 Test 开头的单测,将被一一执行。

执行单元测试:

代码语言:javascript复制
D:codegotest>go test -v ./hello
=== RUN   TestHelloSuite
=== RUN   TestHelloSuite/TestHello
--- PASS: TestHelloSuite (0.00s)
    --- PASS: TestHelloSuite/TestHello (0.00s)
PASS
ok      main/hello      0.149s

测试通过。

8.小结

关于单元测试,本文从 0 到 1 讲解了 Go 如何编写测试用例,熟练掌握 Golang 中单元测试的书写是一位合格 gopher 的必备技能。

推荐使用 testify gomonkey 测试框架编写 Go 的单测,关于其他的单测框架,比如 goconvey gomock,感兴趣的你可自行了解。


参考文献

[1] GoLang快速上手单元测试(思想、框架、实践) [2] golang-单元测试和mock框架的介绍和推荐 [3] gomonkey 1.0 正式发布! [4] Testify - Thou Shalt Write Tests

0 人点赞