不会写单元测试的程序员不是一个合格的滴滴司机
go内置了一套单元测试机制: 利用 go test测试命令
和一套按照约定方式编写的测试函数。
- 测试文件以_test.go 结尾,以_test.go为后缀名编写的go文件不会参与go build的编译过程
- 测试函数有三种:以Test开头的标准测试、 以Benchmark开头的基准性能测试、 以Example开头的样例测试
- go test有两种模式:
- go test 本地目录模式,不带包名,寻找当前目录下的tests go test ./... go test ./foo/...
- go test 指定包名模式: ('go test math') 寻找指定包名下面的tests。
https://stackoverflow.com/questions/16353016/how-to-go-test-all-tests-in-my-project
包内测试函数是串行执行。
golang的约定式测试函数中,会要求传入testing.T testing.B等参数,
有些童鞋会问,我的测试函数不用这个参数,也能跑,
这些参数的作用是充当测试框架和测试函数的桥梁: 测试函数可以通过这个参数,报告日志,控制测试流程,报告测试结果。
1. 标准测试函数
- 导入testing包
- 以Test开头,除Test开头的自定义函数需要首字母大写
- 函数参数t *testing.T用于报告测试失败和附加的日志信息
func TestWriteLog(t *testing.T) {
l := logrus.New()
l.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
})
l.SetOutput(io.Discard) // Send all logs to nowhere by default
bh := &BufferedWriterHook{Writer: os.Stdout}
defer bh.Stop()
err := bh.Fire(&logrus.Entry{Logger: l, Level: logrus.InfoLevel, Message: "test" + time.Now().Format(time.RFC3339)})
if err != nil {
t.Error(t.Name() + " FAIL")
}
}
2. 基准测试函数
- 以Benchmark开头
- b.N表示迭代次数,不固定,确保至少执行1s
func BenchmarkFire(b *testing.B) {
l := logrus.New()
l.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
})
logf, err := os.OpenFile("./log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
panic(err)
}
defer logf.Close()
bh := &BufferedWriterHook{Writer: logf}
defer bh.Stop()
b.ResetTimer() // 重置计时器,忽略前面的准备时间
for n := 0; n < b.N; n++ {
err := bh.Fire(&logrus.Entry{Logger: l, Level: logrus.InfoLevel, Message: "test" + time.Now().Format(time.RFC3339)})
if err != nil {
b.Error(b.Name() + " FAIL")
}
}
}
go test -bench=. 执行基准测试
以上如果有单元测试,也会执行,若要忽略单元测试,请执行go test -bench=. -count 5 -run=^#
//对https://github.com/zwbdzb/logrus-bufferedWriter-hook执行基准测试
BenchmarkFire-8 940003 1130 ns/op
BenchmarkFire1-8 53912 19678 ns/op
前者是循环次数,后者是每次循环的平均耗时。
结果显示 带异步缓冲区的logrus写磁盘能力,是logrus默认同步写磁盘能力的10+倍。
3. 样例函数
- 以Example开头
- 需要在代码内体现预期输出,下方output注释部分。
go test -run=ExampleHook_default
func ExampleHook_default() {
l := logrus.New()
l.SetLevel(logrus.InfoLevel)
l.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
})
l.SetOutput(io.Discard) // Send all logs to nowhere by default
ws := &BufferedWriterHook{Writer: os.Stdout}
defer ws.Stop()
l.AddHook(ws)
l.Info("test2")
l.Warn("test3")
l.Error("test4")
// Output:
// level=info msg=test2
// level=warning msg=test3
// level=error msg=test4
}
4. 用于单元测试的:mock打桩工具
字节开源了一个用于单元测试的打桩工具 : https://github.com/bytedance/mockey
有几个概念先要重申:
mock: 在单元测试中,mock(模拟)指的是一种技术,用于创建一个虚拟的对象,代替真正的对象。通过这种方式,开发人员可以在测试中模拟出某些行为和结果,以此来验证代码的正确性。
Mock对象通常会模拟出真实对象的某些特性,例如方法、属性、状态等等。当测试用例调用这些模拟对象时,它们的行为和结果可以被预设,以此来测试真实对象在不同情况下的行为。
例如,假设我们正在测试一个函数,该函数依赖于一个外部API来执行某些操作。在测试中,我们可以使用mock对象来代替这个API,并预设它在不同情况下返回不同的结果。这样,我们就能够在不依赖于外部API的情况下测试函数的功能,并验证它的正确性。
官方的demo:
import (
"fmt"
"testing"
. "github.com/bytedance/mockey"
. "github.com/smartystreets/goconvey/convey" // . 用在import语法中,表示将引入的包对外暴露的方法和变量导入到当前命名空间
)
func Foo(in string) string {
return in
}
type A struct{}
func (a A) Foo(in string) string { return in }
var Bar = 0
func TestMockXXX(t *testing.T) {
PatchConvey("TestMockXXX", t, func() {
Mock(Foo).Return("c").Build() // mock function, 模拟该Foo函数返回c
Mock(A.Foo).Return("c").Build() // mock method
MockValue(&Bar).To(1) // mock variable ,模拟该变量值是1
So(Foo("a"), ShouldEqual, "c") // assert `Foo` is mocked,
So(new(A).Foo("b"), ShouldEqual, "c") // assert `A.Foo` is mocked
So(Bar, ShouldEqual, 1) // assert `Bar` is mocked
})
// mock is released automatically outside `PatchConvey`
fmt.Println(Foo("a")) // a
fmt.Println(new(A).Foo("b")) // b
fmt.Println(Bar) // 0
}
这个mock 库总体上是个打桩工具,帮助你在单元测试中模拟 某些依赖项的行为和状态。
go test -gcflags="all=-l -N" -v
5. 在单元测试中集成 数据竞争检测
并发读写golang map会panic, 在runtime主动throw出的panic不能被捕获,最终程序crash。
golang 提供了数据竞争检测的记住,协助规避这个问题。
比如:下面使用2个go程序读写map,常规单测不能发现风险。
func RaceDetector() {
done := make(chan bool)
m := make(map[string]string)
go func() {
m["name"] = "data race"
done <- true
}()
fmt.Println("Hello,", m["name"]) // 2个go程分别读写
<-done
}
golangrace detector
需要在运行时才能产生打印警告信息,在生产环境会消耗大量内存和cpu, 目前最佳的实践是在负载测试和单元测试中加入race detector。
go test ./... -race -run=Test_RaceDetector -count=1 分分钟给出告警。
6. 单元测试注意事项
1>. 单测可度量, 开启覆盖率 go test -cover -covermode=count --coverprofile=coverage.out
2>. go test 执行耗时很久, 前面加time 统计具体耗时。
go 测试框架默认的执行超时时间是10min, 这个约束是作用在整个测试套件上,而不是单个测试 ,超过约束时间会终止单测, 可指定 -timeout=30s 修改。
3>. go test 会缓存执行成功的单测,在重复执行时可能影响判断。
显式-count =1禁用缓存
4>. go单元测试的执行顺序心里又要数:
单个包内测试函数默认是串行,所有的测试集是并行执行。
By default, all the tests within a package are run sequentially.
You can also add t.Parallel()
to your tests to make them run in parallel。
Also by default, all of the sets of tests are run in parallel.
本文快速记录了golang单元测试、基准测试、样例测试的写法;
介绍了在单元测试实践中必然会用到的打桩工具和很有用的 数据竞争检测 -race
给出了一些单元测试的日常实践。
耗时3h, 有用指数5颗星。
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/17523100.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化![](https://blog-static.cnblogs.com/files/JulianHuang/QR.gif)