Go: 基于 Ginkgo 框架进行单元测试实践
这是在做 Golang 项目中的一些实践
ginkgo 基于BDD的测试框架; 开始前, 需要花半个小时阅读官方文档
reference
table-driven
一个函数
// TruncateString truncate string to specific length
func TruncateString(s string, n int) string {
if n > len(s) {
return s
}
return s[:n]
}
使用table-driven方式生成测试代码
func TestTruncateString(t *testing.T) {
type args struct {
s string
n int
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := util.TruncateString(tt.args.s, tt.args.n); got != tt.want {
t.Errorf("TruncateString() = %v, want %v", got, tt.want)
}
})
}
}
如果是简单的util函数, 用例比较简单并且用例数量较少, 此时单元测试可读性和可维护性没有问题
table-driven的问题
当涉及业务相关的单元测试, 此时
- 用例复杂, 输入可能是多层嵌套的struct, 某一层的某个变量的值影响输出
- 用例数量多, 某个函数可能有5个以上的用例
由于table-driven的表达能力有限:
- 如何复用输入结构体? (当前情况开发会复制粘贴过去改)
name
过于简单不被重视, 导致单元测试失败时难以快速阅读
最终带来的问题是:
- 单个用例构造复杂, 可能是十几行甚至是几十行;
- 用例和用例之间没有复用, 基本基于复制粘贴;
- 难以区分用例之间的差异
- 用例过多导致难以维护, 不能明确知道每个用例的目的, 用例和用例之间的差别;
- 单个测试集过大, 可能有几百行测试代码;
难以阅读/难以理解/难以维护
Ginkgo解决什么问题?
优势:
- 丰富的表达能力
- 复用: 减少了大量重复代码, 复用
- 让注意力更集中, 只关注每个用例的差异点
- 减少了认知负担, 每个case有明确的描述/上下文/差异
- 更好维护
注意, 实践中:
- 使用
Describe
和Context
描述/表示行为和逻辑, 以及层次结构 - 使用
It
描述及放置每一个用例 - 利用
BeforeEach
构建每个用例(It
)用到的公共内容(setup); 利用AfterEach
执行回收动作(teardown). 此时BeforeEach
用到闭包, 构建闭包变量/类型声明等, 在每个It
中可以复用到 - 使用testify 替换ginkgo的Matcher Using Other Matcher Libraries
-
- 减少目前迁移切换的工作量
-
- testify语法更加简洁直接, ginkgo matcher语法略啰嗦多敲好多字
-
请先阅读官方文档之后, 再开始阅读以下内容
1. 基于ginkgo的table-driven
使用场景: util类的简单测试, input和expect都比较简单
使用
package util_test
import (
"fmt"
"strconv"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
"github.com/stretchr/testify/assert"
"example/pkg/util"
)
var _ = Describe("String", func() {
Describe("TruncateBytes", func() {
var s = []byte("helloworld")
DescribeTable("TruncateBytes cases", func(expected []byte, truncatedSize int) {
assert.Equal(GinkgoT(), expected, util.TruncateBytes(s, truncatedSize))
},
Entry("truncated size less than real size", []byte("he"), 2),
Entry("truncated size equals to real size", s, 10),
Entry("truncated size greater than real size", s, 20),
)
})
})
2. 基于闭包/BeforeEach/It的复用
使用场景: 复杂的输入
Extracting Common Setup: BeforeEach
简单复用
Describe("WithoutResourceType", func() {
var a types.Action
BeforeEach(func() {
a = types.NewAction()
})
It("true", func() {
assert.True(GinkgoT(), a.WithoutResourceType())
})
It("true, empty ResourceType", func() {
a.Attribute.SetResourceTypes([]types.ActionResourceType{})
assert.True(GinkgoT(), a.WithoutResourceType())
})
It("false", func() {
a.Attribute.SetResourceTypes([]types.ActionResourceType{
{
System: "test",
Type: "test",
},
})
assert.False(GinkgoT(), a.WithoutResourceType())
})
}
复杂的复用
Describe("GetPolicesAttrKeys", func() {
var resource *types.Resource
var policies []types.AuthPolicy
BeforeEach(func() {
resource = &types.Resource{
System: "test",
Type: "host",
ID: "1",
Attribute: nil,
}
policies = []types.AuthPolicy{}
})
It("fail", func() {
errExpr := `[{"system": "test", "type": "host", "expression":
{"OR": {"content": [{"NotExists": {"id": []}}]}}}]`
policies = []types.AuthPolicy{
{
Expression: errExpr,
},
}
_, err := GetPolicesAttrKeys(resource, policies)
assert.Error(GinkgoT(), err)
})
})
with patch
Describe("FillSubjectDetail", func() {
var r *request.Request
var ctl *gomock.Controller
var patches *gomonkey.Patches
BeforeEach(func() {
ctl = gomock.NewController(GinkgoT())
r = request.NewRequest()
})
AfterEach(func() {
ctl.Finish()
if patches != nil {
patches.Reset()
}
})
It("pip.GetSubjectPK fail", func() {
patches = gomonkey.ApplyFunc(pip.GetSubjectPK, func(_type, id string) (pk int64, err error) {
return -1, errors.New("get subject_pk fail")
})
err := r.FillSubjectDetail()
assert.Error(GinkgoT(), err)
assert.Contains(GinkgoT(), err.Error(), "get subject_pk fail")
})
})
ginkgo cli
bootstrap & generate
# 进入模块
$ cd pkg/util
# 生成一个模块的 {module_name}_suite_test.go
$ ginkgo bootstrap
# 生成模块下某个文件 {file_name}_test.go
$ ginkgo generate set
run
# 运行当前目录下的
$ ginkgo
# 带tags和gcflags (如果做了mock)
$ ginkgo -tags=jsoniter -gcflags=all=-l
# 递归执行当前目录及所有子目录下的
$ ginkgo -r -v -cover
# 只执行某几个目录
$ ginkgo -r pkg/util pkg/cache
确认覆盖率 make test && make cov
# in Makefile
test:
go test -mod=vendor -gcflags=all=-l $(shell go list ./... | grep -v mock | grep -v docs) -covermode=count -coverprofile .coverage.cov
cov:
go tool cover -html=.coverage.cov