GoLang测试那些事儿

码字不易,转载请附原链,搬砖繁忙回复不及时见谅。

目录

什么是单测

编写单测的好处

何为有效单测

单测函数

测试命令

单用例

命令行打印

html直观展示

服务器直观展示

多用例(Table-Driven Test)

goconvey测试工具

TestMain

基准测试

Example测试

预告


本文源码已上传 Github

什么是单测

单元测试通常是由软件开发⼈员编写和运⾏的⾃动测试,以确保应⽤程序的某个部分(称为“单元”)符合其设计并按预期运⾏。在过程式编程中,⼀个单元可以是⼀个完整的模块,但它更常⻅的是⼀个单独的函数或过程。在⾯向对象的编程中,单元通常是整个接⼝,例如类或单个⽅法。通过⾸先为最⼩的可测试单元编写测试,然后为它们之间的复合⾏为编写测试,可以为复杂的应⽤程序建⽴全⾯的测试。

在开发过程中,软件开发⼈员可能会将标准或已知良好的结果编码到测试中,以验证单元的正确性。在测试⽤例执⾏期间,框架会记录不符合任何条件的测试,并在摘要中报告这些测试。为此,最常⽤的⽅法是测试 - 函数 - 期望值。

概括:通过编写测试代码,并对函数返回结果进行断言,对函数和方法功能进行自动测试,并报告测试结果。

编写单测的好处

  • 发现单元代码逻辑问题

  • 强迫开发人员降低单个函数大小

  • 方便对迭代/重构后的代码进行快速自动化回归测试

  • 提升开发人员自信!

何为有效单测

  • 原则上单测覆盖率应不少于 80%,核心模块或底层逻辑应保证 95% 以上甚至100%

  • 一个 case 一个用例,特别是 || 场景

  • 一定要对结果进行断言

单测函数

testing 为 Go 语言 package 提供自动化测试的支持。通过 go 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 界面特性。

源码和使用文档参考:https://github.com/smartystreets/goconvey/wiki

我们需要在代码中加上包引入代码:

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

使用 GoConvey 书写单元测试,每个测试用例需要使用 Convey 函数包裹起来。它接受的第一个参数为 string 类型的描述;第二个参数一般为 *testing.T,即本例中的变量 t;第三个参数为不接收任何参数也不返回任何值的函数(习惯以闭包的形式书写)。

我们用 Table-Driven Test 进行多用例覆盖,循环内包裹 Convey。

最后,需要使用 So 语句来对条件进行判断。在本例中,我们只使用了 ShouldEqual,表示值应该相等。有关详细的条件列表,可以参见 官方文档

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 标志来执行,多个基准测试按照顺序运行,基准函数会运行目标代码 b.N 次。在基准执行期间,程序会自动调整 b.N 直到基准测试函数持续足够长的时间。

最关键几个参数如下:

  • -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

对上述结果中的每一项,含义如下:

  • -8:表示运行时对应的GOMAXPROCS的值
  • 2124:基准测试的迭代总次数 b.N
  • 545671 ns/op:平均每次迭代所消耗的纳秒数
  • 4654351 B/op:平均每次迭代内存所分配的字节数
  • 30 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,并且尽量减少开发量、尽量减少业务代码侵入,下一期我们将针对这几方面详细介绍,关注我,学习更多后端技术~

参考文章:

https://talks.golang.org/2014/testing.slide#12

第九章 测试 · Go语言标准库

GoTests工具自动化test使用 - 掘金

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AirGo.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值