目录
本文源码已上传 Github
什么是单测单元测试通常是由软件开发⼈员编写和运⾏的⾃动测试,以确保应⽤程序的某个部分(称为“单元”)符合其设计并按预期运⾏。在过程式编程中,⼀个单元可以是⼀个完整的模块,但它更常⻅的是⼀个单独的函数或过程。在⾯向对象的编程中,单元通常是整个接⼝,例如类或单个⽅法。通过⾸先为最⼩的可测试单元编写测试,然后为它们之间的复合⾏为编写测试,可以为复杂的应⽤程序建⽴全⾯的测试。
在开发过程中,软件开发⼈员可能会将标准或已知良好的结果编码到测试中,以验证单元的正确性。在测试⽤例执⾏期间,框架会记录不符合任何条件的测试,并在摘要中报告这些测试。为此,最常⽤的⽅法是测试 - 函数 - 期望值。
概括:通过编写测试代码,并对函数返回结果进行断言,对函数和方法功能进行自动测试,并报告测试结果。
编写单测的好处-
发现单元代码逻辑问题
-
强迫开发人员降低单个函数大小
-
方便对迭代/重构后的代码进行快速自动化回归测试
-
提升开发人员自信!
-
原则上单测覆盖率应不少于 80%,核心模块或底层逻辑应保证 95% 以上甚至100%
-
一个 case 一个用例,特别是 || 场景
-
一定要对结果进行断言
testinggo test
func TestXxx(*testing.T)
注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。
关于测试日志打印,有以下几个方法(src/testing/testing.go):
- Log:调用 fmt.Sprintln 打印信息
- Logf:调用 fmt.Sprintf 打印信息
- Error:调用 fmt.Sprintln 打印日志,标记错误,执行后续代码
- Errorf:调用 fmt.Sprintf 打印日志,标记错误,执行后续代码
- Fatal:调用 fmt.Sprintln 打印日志,标记错误,停止执行
- Fatalf:调用 fmt.Sprintf 打印日志,标记错误,停止执行
当某些测试我们不想执行,但又不想注释代码,可以使用 Ship 方法在当前测试中标记为不执行:
测试命令
测试命令有很多参数,比如 -v 打印详细信息:
go test -v
-cover 打印测试覆盖度:
go test -cover
即打印详情又打印测试覆盖度:
go test -v -cover
-coverprofile=cover.out 保存测试信息到文件,并用不同形式打印
go test -coverprofile=cover.out
go tool cover -func=cover.out
go tool cover -html=cover.out
组和命令生成 html 文件:
go test -gcflags=-l -coverprofile=cover.out && go tool cover -html=cover.out -o cover.html
指定函数:
go test -count=1 -race -v -cover -run ^TestFunc
递归执行整个项目:
go test -count=1 -race -gcflags=-l -coverpkg=./... -coverprofile=cover.out ./... && go tool cover -func=cover.out -o cover.txt && cat cover.txt
go test -count=1 -race -gcflags=-l -cover $(go list ./...) -coverprofile=cover.out ./... && go tool cover -func=cover.out -o cover.txt && cat cover.txt
递归执行整个项目,并忽略 mock 和 vendor 目录:
go test -count=1 -race -gcflags=-l -cover $(go list ./... ) -coverprofile=cover.out ./... && go tool cover -func=cover.out -o cover.txt && cat cover.txt
单用例
unit.go
package unit
func Compare(a, b int) int {
if a > b {
return 1
} else if a == b {
return 0
} else {
return -1
}
}
unit_test.go
package unit
import "testing"
func TestCompare(t *testing.T) {
a := 1
b := 2
want := -1
actual := Compare(a, b)
if actual != want {
t.Errorf("want %d, actual %d", want, actual)
}
}
命令行打印
$ go test -v -cover
=== RUN TestCompare
--- PASS: TestCompare (0.00s)
PASS
coverage: 60.0% of statements
ok gotest/unit 0.559s
html直观展示
$ go test -coverprofile=cover.out
PASS
coverage: 60.0% of statements
ok gotest/unit 0.350s
$ ll
total 24
-rw-r--r-- 1 weihaoyu staff 176 Jan 7 21:53 cover.out
-rw-r--r-- 1 weihaoyu staff 124 Jan 7 20:55 unit.go
-rw-r--r-- 1 weihaoyu staff 191 Jan 7 21:18 unit_test.go
$go tool cover -html=cover.out
会自动在浏览器弹出如下页面:
灰色代表不计入覆盖度统计,红色代表未覆盖,绿色代表已覆盖。
服务器直观展示
假如我们在服务器上在线开发,没有浏览器,想要在浏览器直观地看覆盖率,有办法么?
答案肯定是有的,我们可以先用下面的组合命令生成 html 文件:
go test -coverprofile=cover.out && go tool cover -html=cover.out -o cover.html
然后在对应目录通过 python 的 SimpleHTTPServer 将向目目录作为 http 服务暴露出来:
python -m SimpleHTTPServer 8888
浏览器输入地址+端口号:
www.airgo.com:8888
点击 cover.html 文件即可
我们可以看到上面覆盖度只有 60%,好的单元测试是要覆盖到 80% 以上,那我们需要为每个用力写测试代码么?
有没有一种方法可以定义一个用例组,然后通过循环遍历执行呢?
没错,有!我们可以用下面的 Table-Driven Test
多用例(Table-Driven Test)
思路就是我们上面的思路,定义一个包含如下信息的结构体:
- name 用来描述当前用例场景
- args 代表测试的函数参数
- want 代表我们想要的结果
然后用 range 遍历所有用例,去覆盖到所有情况,我们的 unit_test.go 文件变成如下:
package gotest
import "testing"
func TestCompare(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{name:"a > b", args:args{2, 1}, want:1},
{name:"a == b", args:args{2, 2}, want:0},
{name:"a < b", args:args{1, 2}, want:-1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Compare(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Compare() = %v, want %v", got, tt.want)
}
})
}
}
我们运行看一下结果,这里为了提高阅读体验,只贴命令行的结果:
$ go test -v -cover
=== RUN TestCompare
=== RUN TestCompare/a_>_b
=== RUN TestCompare/a_==_b
=== RUN TestCompare/a_<_b
--- PASS: TestCompare (0.00s)
--- PASS: TestCompare/a_>_b (0.00s)
--- PASS: TestCompare/a_==_b (0.00s)
--- PASS: TestCompare/a_<_b (0.00s)
PASS
coverage: 100.0% of statements
可以看到,覆盖度已经达到了 100% !
有的同学说了,这么多代码,那我每次测试写的代码比我函数还多,岂不是效率很低,有没有办法自动生成测试代码,答案是有,运行如下命令,会将生成的测试代码打印出来:
$ gotests -all unit.go
Generated TestCompare
package gotest
import "testing"
func TestCompare(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Compare(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Compare() = %v, want %v", got, tt.want)
}
})
}
}
把文件复制到当前目录下创建好的 unit_test.go 文件即可,也可以用 -w 参数直接写入到指定文件中,自己再补全用例:
gotests -all -w unit.go unit_test.go
如果用的是 GoLand 工具,可以用 Command + Shift + t 选择自动生成。
如果用的是 VSCode,可以用 Command + T 搜索 "> GO GUT"
goconvey测试工具
goconvey 是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性。
我们需要在代码中加上包引入代码:
import . "github.com/smartystreets/goconvey/convey"
Convey*testing.T
我们用 Table-Driven Test 进行多用例覆盖,循环内包裹 Convey。
SoShouldEqual,表示
Convey
最终 unit_test.go 代码如下:
package gotests
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestCompare(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{name:"a > b", args:args{2, 1}, want:1},
{name:"a == b", args:args{2, 2}, want:0},
{name:"a < b", args:args{1, 2}, want:-1},
}
for _, tt := range tests {
Convey(tt.name, t, func(){
want := Compare(tt.args.a, tt.args.b)
So(want, ShouldEqual, tt.want)
})
}
}
在命令行运行一下:
$go test -v -cover
=== RUN TestCompare
a > b ✔
1 total assertion
a == b ✔
2 total assertions
a < b ✔
3 total assertions
--- PASS: TestCompare (0.00s)
PASS
coverage: 45.5% of statements
ok gotest 0.297s
我们上面说了,goconvey 是一个强大的测试框架,支持很多 Web 界面特性,我们可以在测试的包目录内直接执行 goconvey 命令,会自动弹出 Web 页面执行测试:
TestMain
包级别方法,当前包内单测执行前会先执行该方法一次,典型应用场景:加载全局资源变量,比如下面的日志组件:
package test
import (
"testing"
"xxx/log"
"xxx/resource"
)
func TestMain(m *testing.M) {
resource.Logger = log.New(os.Stdout)
m.Run()
}
基准测试
在 _test.go 结尾的测试文件中,如下形式的函数:
func BenchmarkXxx(*testing.B)
go test-bench
最关键几个参数如下:
- -bench:后接正则表达式,用于匹配压测的函数,如果测试当前目录内所有函数,则用 -bench=.
- -benchtime:后接每个函数执行多久,比如 -benchtime=3s 代表每个函数执行3秒
- -benchmem:输出内存分配
代码如下:
package benchmark
import (
"testing"
)
func MakeSliceWithoutAlloc() []int {
var newSlice []int
for i := 0; i < 100000; i++ {
newSlice = append(newSlice, i)
}
return newSlice
}
func MakeSliceWithPreAlloc() []int{
var newSlice []int
newSlice = make([]int, 0, 100000)
for i := 0; i < 100000; i++ {
newSlice = append(newSlice, i)
}
return newSlice
}
func BenchmarkMakeSliceWithoutAlloc(b *testing.B) {
for i := 0; i < b.N; i++{
MakeSliceWithoutAlloc()
}
}
func BenchmarkMakeSliceWithoutAlloc2(b *testing.B) {
for i := 0; i < b.N; i++{
MakeSliceWithPreAlloc()
}
}
执行结果:
$ go test -bench=. -benchtime=1s -benchmem
goos: darwin
goarch: amd64
pkg: gotest/benchmark
BenchmarkMakeSliceWithoutAlloc-8 2124 545671 ns/op 4654351 B/op 30 allocs/op
BenchmarkMakeSliceWithoutAlloc2-8 8560 141009 ns/op 802818 B/op 1 allocs/op
PASS
ok benchmark 2.563s
对上述结果中的每一项,含义如下:
545671 ns/op4654351 B/op30 allocs/op
Example测试
主要通过对包内函数的举例,起到文档的作用,用 OutPut 标注输出, Unordered output 标注随即输出。
需要特殊注意,如果没有 OutPut 注释,运行 go test 则会跳过该 example。
一般我们用的很少,所以直接给出代码和运行结果:
example.go:
package example
import "fmt"
func SayHello() {
fmt.Println("Hello World")
}
func SayGoodbye() {
fmt.Println("Hello,")
fmt.Println("goodbye")
}
func PrintNames() {
students := make(map[int]string, 4)
students[1] = "Jim"
students[2] = "Bob"
students[3] = "Tom"
students[4] = "Sue"
for _, value := range students {
fmt.Println(value)
}
}
example_test.go:
package example
func ExampleSayHello() {
SayHello()
// OutPut: Hello World
}
func ExampleSayGoodbye() {
SayGoodbye()
// 这个没有OutPut哦~
}
//乱序输出
func ExamplePrintNames() {
PrintNames()
// Unordered output:
// Jim
// Bob
// Tom
// Sue
}
运行如下,可以看到 SayGoodbye 未执行:
$ go test -v -cover
=== RUN ExampleSayHello
--- PASS: ExampleSayHello (0.00s)
=== RUN ExamplePrintNames
--- PASS: ExamplePrintNames (0.00s)
PASS
coverage: 80.0% of statements
ok example 0.533s
预告
本文提到了基础的几个测试场景和使用,但我们真实环境中的测试远比这个复杂,涉及到函数的层级调用、MySQL 操作,Redis 操作、HTTP 调用等,优质的测试是不应该受环境和第三方依赖的,所以我们需要使用一些方法和工具进行 Mock,并且尽量减少开发量、尽量减少业务代码侵入,下一期我们将针对这几方面详细介绍,关注我,学习更多后端技术~
参考文章: