Golang单元测试
testing
import "testing"

Golang支持的测试种类

类型格式作用
单元测试函数名前缀为Test测试程序的一些逻辑行为是否正确
基准(压力)测试函数名前缀为Benchmark测试函数的性能
示例测试函数名前缀为Example为文档提供示例文档
模糊(随机)测试函数名前缀为Fuzz生成一个随机测试用例去覆盖认为测不到的各种赋值场景

单元测试

_test.go
calc.gocalc.goAddMulcalc_test.go
go mod init

实例演示

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

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)
	}
}
Testt *testing.T *testing.B *testing.M
run test
Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestADD$ go_pro

ok  	go_pro	0.272s
go test
PS E:\golang开发学习\go_pro> go test
PASS
ok      go_pro  0.244s

PASS表示测试用例运行成功,FAIL表示测试用例运行失败

go test -v-v-cover
PS E:\golang开发学习\go_pro> go test -v
=== RUN   TestADD
--- PASS: TestADD (0.00s)
PASS
ok      go_pro  0.245s
PS E:\golang开发学习\go_pro> go test -cover
PASS
coverage: 50.0% of statements
ok      go_pro  0.417s
TestADD-run*^$
PS E:\golang开发学习\go_pro> go test -run TestADD -v  
=== RUN   TestADD
--- PASS: TestADD (0.00s)
PASS
ok      go_pro  0.180s

单元测试框架提供的日志方法

方法作用
Log打印日志,同时结束测试
Logf格式化打印日志,同时结束测试
Error打印错误日志,同时结束测试
Errorf格式化打印错误日志,同时结束测试
Fatal打印致命日志,同时结束测试
Fatalf格式化打印致命日志,同时结束测试

子测试(Subtests)

t.Run

calc_test.go

func TestMul(t *testing.T) {
	t.Run("pos", func(t *testing.T) { // pos参数为子测试命名
		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.Errorft.Fatal/t.Fatalft.Error/t.Errorft.Fatal/t.Fatalf

运行结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMul$ go_pro

ok  	go_pro	0.288s

运行某个测试用例的子测试:

PS E:\golang开发学习\go_pro> go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
PASS
ok      go_pro  0.256s
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)
			}
		})
	}
}
cases
  • 新增用例方便
  • 测试代码可读性好
  • 用例失败时,报错信息格式较统一,测试报告易读

若数据很大,或者一些二进制数据,推荐使用相对路径从文件中读取

帮助函数(helpers)

对一些重复的逻辑,抽取出来作为公共的帮助函数可以增加测试代码的可读性和可维护性

借助帮助函数可以让测试用例的主逻辑看起来更加清晰

例如,将创建子测试的逻辑抽取出来:

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}) // 错误case
}

这里故意创建了一个错误的测试用例,运行之后会报告发生错误的文件信息:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMul$ go_pro

--- FAIL: TestMul (0.00s)
    e:\golang开发学习\go_pro\factory_test.go:22: 2 * 0 expected 1, but 0 got
FAIL
FAIL	go_pro	0.259s
FAIL
createMulTestCaseTestMulcreateMulTestCaset.Helper()
t.Helper()
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)
	}
}

运行结果:

Running tool: C:\Go\bin\go.exe test -timeout 30s -run ^TestMul$ go_pro

--- FAIL: TestMul (0.00s)
    e:\golang开发学习\go_pro\factory_test.go:29: 2 * 0 expected 1, but 0 got
FAIL
FAIL	go_pro	0.208s
FAIL
t.Errort.Fatalt.Helper()

setup和teardown

setupteardown
testing
func setup() {
	fmt.Println("before all tests")
}

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

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

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

func TestMain(m *testing.M) {
	
	setup()
	code := m.Run()
	teardown()
	
	os.Exit(code)
}
Test1Test2TestMainTestMain(m)m.Run()os.Exit(code)m.Run()
go test
$ go test
before all tests
test1
test2
PASS
after all tests
ok		example 0.003s

网络测试(Network)

TCP/HTTP

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

package main

import "net/http"

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

则可以创建真是的网络链接来进行测试:

package main

import (
	"io/ioutil"
	"net"
	"net/http"
	"testing"
)

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

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

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

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

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

	if string(body) != "hello world" {
		t.Fatal("expected hello world but got", string(body))
	}
}
net.Listen("tcp", "127.0.0.1:0")http.Serve(l, nil)http.Gethttpnetmock

httptest

net/http/httptest

可将上述测试用例修改为:

package main

import (
	"io/ioutil"
	"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),也达到了相同的目的