Golang-14测试(test)

go test工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件。,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型格式作用
测试函数函数名前缀为Test测试程序的一些逻辑行为是否正确
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example为文档提供示例文档

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

测试函数

测试函数的格式

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

func TestName(t *testing.T){
    // ...
}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

测试函数示例

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。比如,当前 package 有 calc.go 一个文件,我们想测试 calc.go 中的 AddMul 函数,那么应该新建 calc_test.go 作为测试文件。

example/
   |--calc.go
   |--calc_test.go
// example/calc.go

package main

func Add(a int, b int) int {
    return a + b
}

func Mul(a int, b int) int {
    return a * b
}

在当前目录下,我们创建一个calc_test.go的测试文件,并定义一个测试函数如下:

// example/calc_test.go

package main

import "testing"

func TestAdd(t *testing.T) {
	if ans := Add(1, 2); ans != 3 {
		t.Errorf("1 + 2 expected be 3, but %d got", ans)
	}

	if ans := Add(-10, -20); ans != -30 {
		t.Errorf("-10 + -20 expected be -30, but %d got", ans)
	}
}

example包路径下,执行go test命令,可以看到输出结果如下,或 go test -v-v 参数会显示每个用例的测试结果,另外 -cover 参数可以查看覆盖率。

PS C:\Users\kaikai\Desktop\golang\calc> go mod init calc
PS C:\Users\kaikai\Desktop\golang\calc> go test go test
PASS
ok      calc    0.183s

PS C:\Users\kaikai\Desktop\golang\calc> go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      calc    0.419s
PS C:\Users\kaikai\Desktop\golang\calc> go test -cover
PASS
coverage: 50.0% of statements
ok      calc    0.387s

如果其中有很多测试的函数,但是你只想测试其中一个,可以单独执行,例如 TestAdd,可以用 -run 参数指定,该参数支持通配符 *,和部分正则表达式,例如 ^$

PS C:\Users\kaikai\Desktop\golang\calc> go test -run TestAdd -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      calc    0.383s

子测试(Subtests)

子测试是 Go1.7+中新增了子测试内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run创建不同的子测试用例:

// calc_test.go

func TestMul(t *testing.T) {
	t.Run("pos", func(t *testing.T) {
		if Mul(2, 3) != 6 {
			t.Fatal("fail")
		}

	})
	t.Run("neg", func(t *testing.T) {
		if Mul(2, -3) != -6 {
			t.Fatal("fail")
		}
	})
}
  • 之前的例子测试失败时使用 t.Error/t.Errorf,这个例子中使用 t.Fatal/t.Fatalf,区别在于前者遇错不停,还会继续执行其他的测试用例,后者遇错即停。

此时我们再执行go test命令就能够看到更清晰的输出内容了:

PS C:\Users\kaikai\Desktop\golang\calc>  go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
PASS
ok      calc    0.435s

测试组

对于多个子测试的场景,推荐使用(table-driven tests)的写法:

//  calc_test.go
func TestMul(t *testing.T) {

    // 定义一个测试用例类型与存储测试用例的切片
	cases := []struct {
		Name           string
		A, B, Expected int
	}{
		{"pos", 2, 3, 6},
		{"neg", 2, -3, -6},
		{"zero", 2, 0, 0},
	}
    // 遍历切片,逐一执行测试用例
	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if ans := Mul(c.A, c.B); ans != c.Expected {
				t.Fatalf("%d * %d expected %d, but %d got",
					c.A, c.B, c.Expected, ans)
			}
		})
	}
}

所有用例的数据组织在切片 cases 中,看起来就像一张表,借助循环创建子测试。这样写的好处有:

  • 新增用例非常简单,只需给 cases 新增一条测试数据即可。
  • 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
  • 用例失败时,报错信息的格式比较统一,测试报告易于阅读。

如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取。

测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

PS C:\Users\kaikai\Desktop\golang\calc> go test -cover
PASS
coverage: 100.0% of statements
ok      calc    0.406s

从上面的结果可以看到我们的测试用例覆盖了100%的代码。

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

PS C:\Users\kaikai\Desktop\golang\calc>$ go test -cover -coverprofile=calc
PASS
coverage: 100.0% of statements
ok      calc    0.452s

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的calc文件中,然后我们执行go tool cover -html=calc,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。

在这里插入图片描述

上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

基准测试

基准测试函数格式

基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:

func BenchmarkName(b *testing.B){
    // ...
}

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()

基准测试示例

我们为calc包中的Add函数编写基准测试如下:

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if ans := Add(1, 2); ans != 3 {
			b.Errorf("1 + 2 expected be 3, but %d got", ans)
		}
	}
}

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:

PS C:\Users\kaikai\Desktop\golang\calc> $ go test -benchmemem -bench .
goos: windows
goarch: amd64                                           ns/op          0 B/op          0 allocs/op
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkAdd-16         1000000000               0.5880 0 ns/op          0 B/op          0 allocs/op
PASS
ok      calc    1.045s

其中BenchmarkAdd-16表示对Add函数进行基准测试,数字16表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000000.5880 0 ns/op表示每次调用Split函数耗时0.5880ns,这个结果是1000000000次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

PS C:\Users\kaikai\Desktop\golang\calc> $ go test -benchmem -bench . -benchmemktop\golang\cale>
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkAdd-16         1000000000               0.6435 ns/op        0 B/op          0 allocs/op
PASS
ok      calc    1.124s

其中,0 B/op表示每次操作内存分配了小于1字节,0 allocs/op则表示每次操作进行了没有内存分配。

性能比较函数

上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。

性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

例如我们编写了一个计算斐波那契数列的函数如下:

// fib.go

// Fib 是一个计算第n个斐波那契数的函数
func Fib(n int) int {
	if n < 2 {
		return n
	}
	return Fib(n-1) + Fib(n-2)
}

我们编写的性能比较函数如下:

// fib_test.go

func benchmarkFib(b *testing.B, n int) {
	for i := 0; i < b.N; i++ {
		Fib(n)
	}
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }

运行基准测试:

PS C:\Users\kaikai\Desktop\golang\calc> $ go test -bench=Fib
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkFib1-16        296372254                4.055 ns/op
BenchmarkFib2-16        126167323                9.577 ns/op
BenchmarkFib3-16        78145348                15.81 ns/op
BenchmarkFib10-16        2247198               598.1 ns/op
BenchmarkFib20-16          18014             65691 ns/op
BenchmarkFib40-16              2         997968350 ns/op
PASS
ok      calc    13.157s

这里需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。

最终的BenchmarkFib40只运行了两次,每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用-benchtime标志增加最小基准时间,以产生更准确的结果。例如:

PS C:\Users\kaikai\Desktop\golang\calc> $ go test -bench=Fib40 -benchtime=20s
goos: windows
goarch: amd64
pkg: calc
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkFib40-16             22        1024200623 ns/op
PASS
ok      calc    24.025s

这一次BenchmarkFib40函数运行了22次,结果就会更准确一些了。

使用性能比较函数做测试的时候一个容易犯的错误就是把b.N作为输入的大小,例如以下两个例子都是错误的示范:

// 错误示范1
func BenchmarkFibWrong(b *testing.B) {
	for n := 0; n < b.N; n++ {
		Fib(n)
	}
}

// 错误示范2
func BenchmarkFibWrong2(b *testing.B) {
	Fib(b.N)
}

重置时间

b.ResetTimer之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:

func BenchmarkAdd(b *testing.B) {
    time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
	b.ResetTimer()              // 重置计时器
	for i := 0; i < b.N; i++ {
		if ans := Add(1, 2); ans != 3 {
			b.Errorf("1 + 2 expected be 3, but %d got", ans)
		}
	}
}

并行测试

func (b *B) RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。

RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelismRunParallel通常会与-cpu标志一同使用。

func BenchmarkAddParallel(b *testing.B) {
	// b.SetParallelism(1) // 设置使用的CPU数
	for i := 0; i < b.N; i++ {
		if ans := Add(1, 2); ans != 3 {
			b.Errorf("1 + 2 expected be 3, but %d got", ans)
		}
	}
}

还可以通过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的CPU数量。

Setup与TearDown

如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制:测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。

通过在*_test.go文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。

一个使用TestMain来设置Setup和TearDown的示例如下:

func setup() {
	fmt.Println("Before all tests")
}

func teardown() {
	fmt.Println("After all tests")
}

func Test1(t *testing.T) {
	fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
	fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

  • 在这个测试文件中,包含有2个测试用例,Test1Test2
  • 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。
  • 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。
  • 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。

执行 go test,将会输出

PS C:\Users\kaikai\Desktop\golang\calc> $go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok      example 0.006s

网络测试(Network)

TCP/HTTP

假设需要测试某个 API 接口的 handler 能够正常工作,例如 helloHandler

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}
// test code
import (
	"io/ioutil"
	"net"
	"net/http"
	"testing"
)

func handleError(t *testing.T, err error) {
	t.Helper()
	if err != nil {
		t.Fatal("failed", err)
	}
}

func TestConn(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	handleError(t, err)
	defer ln.Close()

	http.HandleFunc("/hello", helloHandler)
	go http.Serve(ln, nil)

	resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
	handleError(t, err)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	handleError(t, err)

	if string(body) != "hello world" {
		t.Fatal("expected hello world, but got", string(body))
	}
}
  • net.Listen("tcp", "127.0.0.1:0"):监听一个未被占用的端口,并返回 Listener。
  • 调用 http.Serve(ln, nil) 启动 http 服务。
  • 使用 http.Get 发起一个 Get 请求,检查返回值是否正确。
  • 尽量不对 httpnet 库使用 mock,这样可以覆盖较为真实的场景。

httptest

针对 http 开发的场景,使用标准库 net/http/httptest 进行测试更为高效。

// test code
import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestConn(t *testing.T) {
	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	w := httptest.NewRecorder()
	helloHandler(w, req)
	bytes, _ := ioutil.ReadAll(w.Result().Body)

	if string(bytes) != "hello world" {
		t.Fatal("expected hello world, but got", string(bytes))
	}
}

使用 httptest 模拟请求对象(req)和响应对象(w),达到了相同的目的。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值