不会写单元测试的程序员不是一个合格的滴滴司机

go内置了一套单元测试机制: 利用 go test测试命令和一套按照约定方式编写的测试函数。

  1. 测试文件以 _test.go 结尾
  2. 测试函数以Test、 Benchmark 、 Example开头
  3. go test有两种模式:
  • go test 本地目录模式,不带包名,寻找当前目录下的tests
  • go test 指定包名模式: ('go test math', 'go test ./...', and even 'go test .') 寻找指定包名下面的tests。

在包目录内,所有以_test.go为后缀名编写的go文件不会参与go build的编译过程.

go test 一共三种测试函数:

  • 标准测试函数, 函数以Test为前缀,用于测试逻辑行为正确性, go test 会报告测试结果 PASS、FAIL
  • 基准测试函数是以Benchmark为前缀的函数,用于衡量函数性能, 拿到平均执行时间
  • 样例函数, 提供一个编译器保证正确性的示例文档

golang的约定式测试函数中,会要求传入testing.T testing.B等参数,
有些童鞋会问,我的测试函数不用这个参数,也能跑,
这些参数的作用是充当测试框架和测试函数的桥梁: 测试函数可以通过这个参数,报告日志,控制测试流程,报告测试结果。

标准测试函数

  • 导入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")
	}
}

基准测试函数

  • 以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+倍。

样例函数

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

用于单元测试的: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

本文快速记录了golang单元测试、基准测试、样例测试的写法,介绍了在单元测试中可能用到的打桩工具, 耗时3h, 有用指数4颗星。



btw 本文测试源码位于https://github.com/zwbdzb/logrus-bufferedWriter-hook, 这是一个带异步缓冲区的logrus日志Hook,能有效解决logrus默认不支持异步日志带来的写性能问题,欢迎试用,期待你的star。

下面是相比同步写日志的基准测试结果:大致是原同步写日志的 10s+倍性能。

-8表示8个CPU线程执行;64819表示总共执行了64819次;19755ns/op,表示每次执行耗时19755纳秒;496/op表示每次执行分配了496字节内存;15 allocs/op表示每次执行分配了15次对象。

posted @ 2023-07-03 15:46  博客猿马甲哥  阅读(505)  评论(1编辑  收藏  举报