程序开发中比较重要的一点是如何可以高效的进行单元测试,可以保证快速发现定位问题,在 GoLang 中自带了一个轻量级的测试框架 testing
以及 go test
命令来实现单元测试和性能测试。
简介
假设有一个简单的除法实现,对应文件 math.go
。
package math
import (
"errors"
)
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisor should not be 0")
}
return a / b, nil
}
与之对应的单元测试文件 math_test.go
为。
package math
import "testing"
func TestDivision(t *testing.T) {
if rc, err := Division(6, 2); rc != 3 || err != nil {
t.Fatal("invalid division result.")
} else {
t.Log("division test pass.")
}
}
func TestDivisionFail(t *testing.T) {
if _, err := Division(6, 0); err == nil {
t.Error("division did not work as expected.")
} else {
t.Log("divisor 0 pass.", err)
}
}
那么对应的单元测试的示例如下,需要确保遵循如下的规则:
- 文件名需要以
_test.go
结尾,在执行go test
时会自动匹配对应代码; - 测试文件引入
testing
包,且所有的测试用例必须以Test
开头; - 可以通过测试函数的入参
t *testing.T
来标记错误状态信息;
在测试函数中,可以通过调用 testing.T
中定义的相关方法来标记测试信息。
Fail()
标记失败,但继续执行当前测试函数;FailNow()
失败,立即终止当前测试函数执行;Log()
输出错误或者调试信息;Error()
等价于Fail + Log
;Fatal()
等价于FailNow + Log
;Skip()
跳过当前函数,通常用于未完成的测试用例。
然后可以通过 go test
执行测试,如果通过,默认只会打印通过的信息,可以通过 go test -v
查看详情。
$ go test -v
=== RUN TestDivision
--- PASS: TestDivision (0.00s)
math_test.go:9: division test pass.
=== RUN TestDivisionFail
--- PASS: TestDivisionFail (0.00s)
math_test.go:17: divisor 0 pass. divisor should not be 0
PASS
ok _/workspace/golang/math 0.002s
在执行时,也可以通过 -run="TestTwo"
运行具体的测试用例。
覆盖率
生成覆盖率很简单,在运行 go test
时指定 -coverprofile=cover.out
参数收集覆盖率数据,然后通过 go tool cover -html=cover.out -o coverage.html
生成文本或者 html 可视化报告即可。
testify
Testify - Thou Shalt Write Tests 这是一个更加友好基于 GoLang 测试库的 Assert 方案,相比来说,提供了更加友好的提示,使用也更加广泛。
如下是官方提供的简单示例。
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// assert equality
assert.Equal(t, 123, 123, "they should be equal")
// assert inequality
assert.NotEqual(t, 123, 456, "they should not be equal")
// assert for nil (good for errors)
assert.Nil(t, object)
// assert for not nil (good when you expect something)
if assert.NotNil(t, object) {
// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal(t, "Something", object.Value)
}
}
HTTPTest
对于 Web 来说,同样提供了 httptest 的测试工具。
示例
如下是实现的一个简单的 HealthCheck 接口。
// FILE: ping.go
package main
import (
"log"
"net/http"
)
func PingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"alive":true}`))
}
func main() {
http.HandleFunc("/ping", PingHandler)
if err := http.ListenAndServe(":12345", nil); err != nil {
log.Fatal("Listen failed: ", err)
}
}
// FILE: ping_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestPingHandler(t *testing.T) {
// 用来构建模拟请求数据
req, err := http.NewRequest("GET", "/ping", nil)
if err != nil {
t.Fatal(err)
}
// 满足ResponseWriter可以用来记录返回的响应
rr := httptest.NewRecorder()
PingHandler(rr, req)
if s := rr.Code; s != http.StatusOK {
t.Errorf("Invalid status code, %d != %d", s, http.StatusOK)
}
e := `{"alive":true}`
if rr.Body.String() != e {
t.Errorf("Invalid body, '%s' != '%s'", rr.Body.String(), e)
}
}
然后可以通过 go test
进行测试。
Mock
官方提供了一个 gomock 库,包括一个基本的示例 sample,包含了基础的库和一个辅助代码生成工具 mockgen
(一个可执行文件),可以通过如下命令安装。
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
然后可以通过 mockgen 命令生成 mock 对象的 .go
文件。
----- 将foo.go里的所有接口mock到foo_mock.go文件中,对应的包名为foo
$ mockgen -destination foo_mock.go -source foo.go -package foo
性能测试
除了上述的单元测试,还可以通过该模块执行一些简单的压力测试,默认不会执行性能测试,可以通过 -test.bench=".*"
执行对应的压力测试用例。
例如,在 math_test.go
文件中,同时添加性能测试代码。
package math
import "testing"
func BenchmarkDivision(b *testing.B) {
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
func BenchmarkDivisionConsuming(b *testing.B) {
b.StopTimer()
// some init stuff.
b.StartTimer()
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
如上性能测试中,通过 b.StopTimer()
b.StartTimer()
可暂定对时间的计时,在这中间可以做些初始化的操作。
$ go test -test.bench=".*"
goos: linux
goarch: amd64
BenchmarkDivision-8 2000000000 0.39 ns/op
BenchmarkDivisionConsuming-8 2000000000 0.39 ns/op
PASS
ok _/workspace/golang/math 1.655s
也可以通过 go test -bench=.
进行性能测试。
其它
表驱动
也就是直接定义一个临时的表结构,然后进行检查。
func TestConvert(t *testing.T) {
cases := []struct { from, to, expected string} {
{"50mi", "km", "80.47km" },
}
for _, c := range cases {
str, err := Convert(c.from, c.to)
if err != nil {
t.Log("error should be nil", err)
t.Fail()
}
if str != expected {
t.Log("error should be"+expected+", but got", str)
t.Fail()
}
}
}