单元测试的定义
单元测试是用来验证代码的正确性
被验证的代码可以是一个模块,一个类,一个函数或者方法
正确性是指在给定的输入下,总能得到预期的输出
本文会分析golang原生单元测试存在的一些问题,以及如何使用goconvey和mockey等框架解决这些问题
golang原生单元测试
快速入门:
go
package test
import (
"strings"
"testing"
)
func funcA(s string) string {
return s
}
// 执行整个包的单元测试 go test -v 其中-v表示打印测试函数的所有细节
// 指定运行某一测试函数 go test -run TestFunA -v
func TestFunA(t *testing.T) {
if !strings.EqualFold(funcA("aa"), "aaa") {
t.Errorf("Test Case1 fail")
}
if !strings.EqualFold(funcA("bbb"), "bbb") {
t.Errorf("Test Case2.1 bbb")
}
if !strings.EqualFold(funcA("cc"), "ccc") {
t.Errorf("Test Case2.2 hello")
}
}
进入到当前文件所在目录后,命令行输入:go test -run TestFunA -v,单元测试结果如下:
lua
=== RUN TestFunA
native_test.go:22: Test Case1 fail
native_test.go:28: Test Case2.2 hello
--- FAIL: TestFunA (0.00s)
FAIL
exit status 1
FAIL xxx/framework/all/test2 1.417s
这样我们就完成了使用golang原生单元测试来测试我们代码逻辑的工作
存在的问题
原生单元测试主要存在以下两个大的问题:
- 缺少断言函数,输出不够直观简洁,没有层级关系
- 不支持mock功能,如果测试的代码对外部有依赖,可能没办法满足正确性
为了解决以上两个问题,我们引入了goconvey和mockey
- goconvey:自带丰富的断言函数,支持多层级嵌套单测,输出清晰的单测结果
- mockey:支持mock功能,一般结合goconvey一起使用
goconvey
go mod
arduino
go get github.com/smartystreets/goconvey
快速入门
go
package test
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func funcA(s string) string {
return s
}
// go test -run TestConveyFuncA -v
func TestConveyFuncA(t *testing.T) {
// 最外层Convey函数签名:Convey(description string, t *testing.T, action func())
Convey("TestConveyHello", t, func() {
// 不是最外层的Convey,不需要参数t,函数签名:Convey(description string, action func())
Convey("Test Case1", func() {
// 支持丰富的断言
So(funcA("aa"), ShouldEqual, "aaa")
})
Convey("Test Case2", func() {
// Convey可以无限嵌套
Convey("Test Case2.1", func() {
So(funcA("bbb"), ShouldEqual, "bbb")
})
Convey("Test Case2.2", func() {
So(funcA("cc"), ShouldEqual, "hello")
})
})
})
}
进入到当前文件所在目录后,命令行输入:go test -run TestConveyFuncA -v,单元测试结果如下:
bash
=== RUN TestConveyFuncA
TestConveyHello
Test Case1 ✘
Test Case2
Test Case2.1 ✔
Test Case2.2 ✘
Failures:
* xxx/framework/all/test2/convey_test.go
Line 18:
Expected: "aaa"
Actual: "aa"
(Should equal)!
Diff: '"aaa"'
* xxx/framework/all/test2/convey_test.go
Line 27:
Expected: "hello"
Actual: "cc"
(Should equal)!
3 total assertions
--- FAIL: TestConveyFuncA (0.00s)
FAIL
exit status 1
FAIL xxx/framework/all/test2 1.425s
本文不对goconvey做过多的展开讨论,感兴趣的同学可以多探索下goconvey的其他用法
mockey
字节开源的golang单元测试框架,支持mock功能,一般结合goconvey一起使用
功能概览如下:
-
变量
-
基础mock
- 普通变量
- 函数变量
-
-
函数/方法
-
基础mock
- 普通函数
- 普通方法
- 私有类型的方法
- 匿名struct的方法
-
其他功能
- goroutine 条件过滤
- 获取原函数执行次数
- 获取mock函数执行次数
-
go mod
kotlin
go get github.com/bytedance/mockey@latest
mock变量
常用api
scss
// 开始mock
// targetPtr:需要mock的变量地址
func MockValue(targetPtr interface{}) *MockerVar
// 设置变量
// value:mock的新值
func (mocker *MockerVar) To(value interface{}) *MockerVar
// 手动取消mock
func (mocker *MockerVar) UnPatch() *MockerVar
// 手动再次mock
func (mocker *MockerVar) Patch() *MockerVar
demo
scss
package test
import (
. "github.com/bytedance/mockey"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
var (
one = "one"
)
// go test -run TestMockVar -v
func TestMockVar(t *testing.T) {
PatchConvey("mock变量", t, func() {
// PatchConvey执行结束后,自动释放内部的patch,免去defer的苦恼
PatchConvey("mock普通变量", func() {
MockValue(&one).To("one v2")
So(one, ShouldEqual, "one v2")
})
a := 10
PatchConvey("mock函数变量", func() {
MockValue(&a).To(20)
So(a, ShouldEqual, 20)
})
PatchConvey("手动取消mock,手动再次mock", func() {
mockB := MockValue(&a).To(20)
So(a, ShouldEqual, 20)
// 手动取消mock
mockB.UnPatch()
So(a, ShouldEqual, 10)
// 手动再次mock
mockB.Patch()
So(a, ShouldEqual, 20)
})
})
}
进入到当前文件所在目录后,命令行输入:go test -run TestMockVar -v,单元测试结果如下:
diff
=== RUN TestMockVar
mock变量
mock普通变量 ✔
mock函数变量 ✔
手动取消mock,手动再次mock ✔✔✔
5 total assertions
--- PASS: TestMockVar (0.00s)
PASS
ok xxx/framework/all/test2 1.349s
PatchConvey函数
常用api
go
// 替代goconvey的Convey函数,用法与Convey完全一致
// 不同点:自动释放当前convey内部的patch,免去defer的苦恼
func PatchConvey(items ...interface{})
demo已经在上面mock变量中给出
mock函数/方法
常用api
scss
// 开始mock
// target:需要mock的函数/方法
func Mock(target interface{}, opt ...optionFn) *MockBuilder
// mock方式一:直接设置结果
// results参数列表需要完全等同于需要mock的函数返回值列表
func (builder *MockBuilder) Return(results ...interface{}) *MockBuilder
// mock方式二:使用mock函数
// hook 参数与返回值需要与mock函数完全一致,注意类成员函数需要增加self作为第一个参数(目前已经兼容了不传入receiver,当不需要使用的时候可以忽略)
func (builder *MockBuilder) To(hook interface{}) *MockBuilder
// 可选项
// 条件设置
// when:表示在何种条件下调用mock函数返回mock结果
// 函数原型:when(args...) bool
// args:与Mock 函数参数一致,一般通过args来判断是否需要执行 mock,注意类成员函数需要增加self作为第一个参数(目前已经兼容了不传入receiver,当不需要使用的时候可以忽略)
// 返回值:bool。是true的时候执行mock
func (builder *MockBuilder) When(when interface{}) *MockBuilder
// mock访问goroutine限制
// 只在当前goroutine执行mock
func (builder *MockBuilder) IncludeCurrentGoRoutine() *MockBuilder
// 不再当前goroutine执行mock
func (builder *MockBuilder) ExcludeCurrentGoRoutine() *MockBuilder
// 过滤指定的goroutine
// filter:过滤类型Disable = 0不启用mock,Include = 1,Exclude = 2
// gId:指定的goroutine,可通过工具函数获取,工具函数下面会介绍
func (builder *MockBuilder) FilterGoRoutine(filter FilterGoroutineType, gId int64) *MockBuilder
// 创建mock
func (builder *MockBuilder) Build() *Mocker
// 手动取消mock代理
func (mocker *Mocker) UnPatch() *Mocker
// 手动启用mock代理
func (mocker *Mocker) Patch() *Mocker
// mock的统计结果,一般用于结果断言。注意每次重新mock或修改mock都会重置为0
// 被mock函数调用的次数
func (mocker *Mocker) Times() int
// hook函数调用的次数
func (mocker *Mocker) MockTimes() int
mock函数demo:
go
package test
import (
. "github.com/bytedance/mockey"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func funcA(s string) string {
return s
}
// go test -run TestMockFunc -v -gcflags="all=-l -N"
// 使用-gcflags="all=-l -N",禁用内联和编译优化
func TestMockFunc(t *testing.T) {
PatchConvey("mock函数方式1", t, func() {
Mock(funcA).Return("mock s").Build()
So(funcA("hello"), ShouldEqual, "mock s")
})
PatchConvey("mock函数方式2", t, func() {
Mock(funcA).To(func(s string) string {
return "mock s"
}).Build()
So(funcA("hello"), ShouldEqual, "mock s")
})
PatchConvey("mock函数,使用when来决定是否需要mock", t, func() {
Mock(funcA).When(func(s string) bool {
return s == "hello1"
}).To(func(s string) string {
return "mock s"
}).Build()
So(funcA("hello1"), ShouldEqual, "mock s")
So(funcA("hello"), ShouldEqual, "mock s")
})
PatchConvey("mock函数,手动取消mock,手动再次mock", t, func() {
m := Mock(funcA).To(func(s string) string {
return "mock s"
}).Build()
m.IncludeCurrentGoRoutine()
So(funcA("hello"), ShouldEqual, "mock s")
So(m.Times(), ShouldEqual, 1)
So(m.MockTimes(), ShouldEqual, 1)
// 手动取消
m.UnPatch()
So(funcA("hello"), ShouldEqual, "hello")
// 手动再次mock
m.Patch()
So(funcA("hello"), ShouldEqual, "mock s")
})
}
进入到当前文件所在目录后,命令行输入:go test -run TestMockFunc -v -gcflags="all=-l -N",单元测试结果如下:
less
=== RUN TestMockFunc
mock函数方式1 ✔
1 total assertion
mock函数方式2 ✔
2 total assertions
mock函数,使用when来决定是否需要mock ✔✘
Failures:
* xxx/framework/all/test2/mockey_test.go
Line 78:
Expected: "mock s"
Actual: "hello"
(Should equal)!
4 total assertions
mock函数,手动取消mock,手动再次mock ✔✔✔✔✔
9 total assertions
--- FAIL: TestMockFunc (0.00s)
FAIL
exit status 1
FAIL xxx/framework/all/test2 2.087s
mock方法demo
go
package test
import (
. "github.com/bytedance/mockey"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
type Class struct {
}
func (Class) FunA(s string) string {
return s
}
func (*Class) FunB(s string) string {
return s
}
// go test -run TestMockMethod -v -gcflags="all=-l -N"
func TestMockMethod(t *testing.T) {
PatchConvey("mock方法方式1", t, func() {
PatchConvey("mock方法方式1.1 - 非指针", func() {
Mock(Class.FunA).Return("mock s").Build()
So(Class{}.FunA("hello"), ShouldEqual, "mock s")
})
PatchConvey("mock方法方式1.2 - 指针", func() {
Mock((*Class).FunB).Return("mock s").Build()
So((&Class{}).FunB("hello"), ShouldEqual, "mock s")
})
})
PatchConvey("mock方法方式2", t, func() {
PatchConvey("mock方法方式2.1 - 非指针", func() {
Mock(Class.FunA).To(func(self Class, s string) string {
return "mock s"
}).Build()
So(Class{}.FunA("hello"), ShouldEqual, "mock s")
})
PatchConvey("mock方法方式2.2 - 指针", func() {
Mock((*Class).FunB).To(func(self *Class, s string) string {
return "mock s"
}).Build()
So((&Class{}).FunB("hello"), ShouldEqual, "mock s")
})
})
}
进入到当前文件所在目录后,命令行输入:go test -run TestMockMethod -v -gcflags="all=-l -N",单元测试结果如下:
markdown
=== RUN TestMockMethod
mock方法方式1
mock方法方式1.1 - 非指针 ✔
mock方法方式1.2 - 指针 ✔
2 total assertions
mock方法方式2
mock方法方式2.1 - 非指针 ✔
mock方法方式2.2 - 指针 ✔
4 total assertions
--- PASS: TestMockMethod (0.00s)
PASS
ok xxx/framework/all/test2 1.400s
工具函数
go
// 作用:mock私有类型的方法 或 mock匿名struct的方法,获取不到会panic
// 参数:
// instance:私有struct实例 或 含有多层嵌套匿名struct的struct实例
// methodName:对应方法名,必须是public方法
func GetMethod(instance interface{}, methodName string) interface{}
// 获取当前goroutine id,已过时,不推荐使用
func GetGoroutineId() int64
demo:
go
package test
import (
"fmt"
. "github.com/bytedance/mockey"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
type IReader interface {
Get(key string) string
}
type reader struct {
*Client1
}
func (r *reader) Get(s string) string {
return r.Client1.GetKey(s)
}
func NewReader(c *Client1) IReader {
return &reader{
Client1: c,
}
}
type Client1 struct {
client2
}
type client2 struct {
}
func (c *client2) GetKey(key string) string {
return key
}
// go test -run TestGetMethod -v -gcflags="all=-l -N"
func TestGetMethod(t *testing.T) {
PatchConvey("工具类", t, func() {
PatchConvey("使用GetMethod mock私有类型的方法", func() {
r := NewReader(nil)
Mock(GetMethod(r, "Get")).To(func(s string) string {
return "aaa"
}).Build()
fmt.Println(r.Get(""))
})
PatchConvey("使用GetMethod mock匿名struct的方法", func() {
r := NewReader(&Client1{})
Mock(GetMethod(r, "GetKey")).To(func(s string) string {
return "bbb"
}).Build()
fmt.Println(r.Get(""))
})
PatchConvey("GetGoroutineId获取当前goroutine id", func() {
fmt.Println(GetGoroutineId())
})
})
}
进入到当前文件所在目录后,命令行输入:go test -run TestGetMethod -v -gcflags="all=-l -N",单元测试结果如下:
markdown
=== RUN TestGetMethod
工具类
使用GetMethod mock私有类型的成员函数 aaa
使用GetMethod mock匿名struct的成员函数 bbb
GetGoroutineId获取当前goroutine id 6
0 total assertions
--- PASS: TestGetMethod (0.00s)
PASS
ok xxx/framework/all/test2 0.613s
总结
本文重点分享了如何使用goconvey + mockey框架来完成我们的单元测试,帮助我们验证代码的正确性。也欢迎大家在日常工作中多多实践,多分享交流经验