go Test

140 阅读2分钟

[ GO Test | 青训营笔记 ]

测试文件以 _test.go结尾,测试函数以 Test+测试方法名命名。测试参数有且仅有一个(t *testing.T),基准测试参数*testing.BTestMain 的参数是 *testing.M 类型。

测试指令:go test,测试所有测试文件

go test -v

  • -v 参数会显示每个用例的测试结果;
  • -cover 参数可以查看覆盖率;
  • -run 参数指定测试用例,该参数支持通配符 *,和部分正则表达式,例如 ^、$

子测试(Subtest)

在测试函数里通过 t.Run()创建子测试:

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")
		}
	})
}
> go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)
--- PASS: TestMul/pos (0.00s)
PASS
ok      example 0.055s

对于多个子测试的场景,更推荐如下的写法(table-driven tests)

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)
			}
		})
	}
}

帮助函数(helpers)

package main

import "testing"

type calcCase struct{ A, B, Expected int }

func createMulTestCase(t *testing.T, c *calcCase) {
	t.Helper()
	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)
	}

}

func TestMul(t *testing.T) {
	createMulTestCase(t, &calcCase{2, 3, 6})
	createMulTestCase(t, &calcCase{2, -3, -6})
	createMulTestCase(t, &calcCase{2, 0, 1}) // wrong case
}

t.Helper()用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数的内部信息。

关于 helper 函数的 2 个建议:

  • 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
  • 调用 t.Helper() 让报错信息更准确,有助于定位。

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

执行 go test,将会输出

$ 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"))
}

网络连接进行测试:

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 进行测试更为高效。

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),达到了相同的目的。

Benchmark 基准测试

func BenchmarkName(b *testing.B){
    // ...
}
  • 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
  • 参数为 b *testing.B
  • 执行基准测试时,需要添加 -bench 参数。

例如:

func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}
$ go test -benchmem -bench .
...
BenchmarkHello-16   15991854   71.6 ns/op   5 B/op   1 allocs/op
...

基准测

试报告每一列值对应的含义如下:

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器

使用 RunParallel 测试并发性能:

func BenchmarkParallel(b *testing.B) {
	templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
	b.RunParallel(func(pb *testing.PB) {
		var buf bytes.Buffer
		for pb.Next() {
			// 所有 goroutine 一起,循环一共执行 b.N 次
			buf.Reset()
			templ.Execute(&buf, "World")
		}
	})
}
$ go test -benchmem -bench .
...
BenchmarkParallel-16   3325430     375 ns/op   272 B/op   8 allocs/op
...