unit test definition
Unit testing is used to verify the correctness of the code
The code being verified can be a module, a class, a function or a method
Correctness means that given input, the expected output can always be obtained
This article will analyze some problems existing in golang's native unit testing and how to use frameworks such as goconvey and mockey to solve these problems.
golang native unit testing
Quick Start:
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")
}
}
After entering the directory where the current file is located, enter the command line: go test -run TestFunA -v. The unit test results are as follows:
=== 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
In this way, we have completed the work of using golang native unit testing to test the logic of our code
Problems
There are two main problems with native unit testing:
- Lack of assertion function, output not intuitive and concise, no hierarchical relationship
- The mock function is not supported. If the tested code has external dependencies, it may not be able to meet the correctness requirements.
In order to solve the above two problems, we introduced goconvey and mockey
- goconvey: comes with rich assertion functions, supports multi-level nested single tests, and outputs clear single test results.
- mockey: supports mock function, generally used in conjunction with goconvey
goconvey
go mod
go get github.com/smartystreets/goconvey
Quick start
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")
})
})
})
}
After entering the directory where the current file is located, enter the command line: go test -run TestConveyFuncA -v. The unit test results are as follows:
=== 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
This article will not discuss goconvey too much. Interested students can explore other uses of goconvey.
mockey
Byte's open source golang unit testing framework supports mock function and is generally used in conjunction with goconvey.
An overview of the features is as follows:
-
variable
-
Basic mock
- Ordinary variables
- function variable
-
-
function/method
-
Basic mock
- Ordinary function
- Ordinary method
- Private type methods
- Methods of anonymous struct
-
Other functions
- goroutine conditional filtering
- Get the number of executions of the original function
- Get the number of mock function executions
-
go mod
go get github.com/bytedance/mockey@latest
mock variable
Commonly used APIs
// 开始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
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)
})
})
}
After entering the directory where the current file is located, enter the command line: go test -run TestMockVar -v. The unit test results are as follows:
=== RUN TestMockVar
mock变量
mock普通变量 ✔
mock函数变量 ✔
手动取消mock,手动再次mock ✔✔✔
5 total assertions
--- PASS: TestMockVar (0.00s)
PASS
ok xxx/framework/all/test2 1.349s
PatchConvey function
Commonly used APIs
=== RUN TestMockVar
mock变量
mock普通变量 ✔
mock函数变量 ✔
手动取消mock,手动再次mock ✔✔✔
5 total assertions
--- PASS: TestMockVar (0.00s)
PASS
ok xxx/framework/all/test2 1.349s
The demo has been given in the mock variable above
mock function/method
Commonly used APIs
// 开始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 function demo:
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")
})
}
After entering the directory where the current file is located, enter the command line: go test -run TestMockFunc -v -gcflags="all=-l -N". The unit test results are as follows:
=== 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 method demo
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")
})
})
}
After entering the directory where the current file is located, enter the command line: go test -run TestMockMethod -v -gcflags="all=-l -N". The unit test results are as follows:
=== 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
Utility function
// 作用:mock私有类型的方法 或 mock匿名struct的方法,获取不到会panic
// 参数:
// instance:私有struct实例 或 含有多层嵌套匿名struct的struct实例
// methodName:对应方法名,必须是public方法
func GetMethod(instance interface{}, methodName string) interface{}
// 获取当前goroutine id,已过时,不推荐使用
func GetGoroutineId() int64
demo:
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())
})
})
}
After entering the directory where the current file is located, enter the command line: go test -run TestGetMethod -v -gcflags="all=-l -N". The unit test results are as follows:
=== 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
Summarize
This article focuses on sharing how to use the goconvey + mockey framework to complete our unit testing and help us verify the correctness of the code. We also welcome everyone to practice more in daily work and share more experiences.
Finally: The complete software testing video tutorial below has been compiled and uploaded. Friends who need it can get it by themselves [guaranteed 100% free]
Software Testing Interview Document
We must study to find a high-paying job. The following interview questions are the latest interview materials from first-tier Internet companies such as Alibaba, Tencent, Byte, etc., and some Byte bosses have given authoritative answers. After finishing this set I believe everyone can find a satisfactory job based on the interview information.