Go 语言基础之单元测试
本文介绍 Go 语言的单元测试。
单元测试基础
Go 语言依赖go test
命令进行函数测试,包目录下所有以_test.go
为后缀的文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件。在*_test.go
文件中,可以编写以下三种类型的函数:
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test |
测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark |
测试函数的性能 |
示例函数 | 函数名前缀为Example |
为文档提供示例文档 |
上述函数中,最常使用的就是测试函数。
testing.T
☕️ 常用方法
// 单元测试函数必须导入 testing 包,函数前缀名必须以 Test 开头,后缀名必须以大写字母开头
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
// 参数 t 是用于报告测试失败和附加的日志信息。testing.T 的拥有的方法如下:
// Log 使用与 Println 相同的格式化语法对它的参数进行格式化,然后将格式化后的文本记录到错误日志里面
// 1) 对于测试来说,格式化文本只会在测试失败或者设置了 test -v 标志的情况下被打印出来
// 2) 对于基准测试来说,为了避免 test -v 标志的值对测试的性能产生影响, 格式化文本总会被打印出来
func (c *T) Log(args ...interface{})
// Log 使用与 Printf 相同的格式化语法对它的参数进行格式化,然后将格式化后的文本记录到错误日志里面
// 如果输入的格式化文本最末尾没有出现新行,那么将一个新行添加到格式化后的文本末尾
// 1) 对于测试来说,Logf 产生的格式化文本只会在测试失败或者设置了 test -v 标志的情况下被打印出来
// 2) 对于基准测试来说,为了避免 test -v 标志的值对测试的性能产生影响,Logf 产生的格式化文本总会被打印出来
func (c *T) Logf(format string, args ...interface{})
// 将当前测试标识为失败,但是仍继续执行该测试
func (c *T) Fail()
// 将当前测试标识为失败并停止执行该测试,在此之后,测试过程将在下一个测试或者下一个基准测试中继续
// FailNow 必须在运行测试函数或者基准测试函数的 goroutine 中调用,而不能在测试期间创建的 goroutine 中调用。调用 FailNow 不会导致其他 goroutine 停止
func (c *T) FailNow()
// Failed 用于报告测试函数是否已失败
func (c *T) Failed() bool
// 调用 Error 相当于在调用 Log 之后调用 Fail
func (c *T) Error(args ...interface{})
// 调用 Errorf 相当于在调用 Logf 之后调用 Fail
func (c *T) Errorf(format string, args ...interface{})
// 调用 Fatal 相当于在调用 Log 之后调用 FailNow
func (c *T) Fatal(args ...interface{})
// 调用 Fatalf 相当于在调用 Logf 之后调用 FailNow
func (c *T) Fatalf(format string, args ...interface{})
// 将当前测试标识为“被跳过”并停止执行该测试。 如果一个测试在失败(参考 Error、Errorf 和 Fail)之后被跳过了, 那么它还是会被判断为是“失败的”
// 在停止当前测试之后,测试过程将在下一个测试或者下一个基准测试中继续,具体请参考 FailNow
// SkipNow 必须在运行测试的 goroutine 中进行调用,而不能在测试期间创建的 goroutine 中调用。 调用 SkipNow 不会导致其他 goroutine 停止
func (c *T) SkipNow()
// 调用 Skip 相当于在调用 Log 之后调用 SkipNow
func (c *T) Skip(args ...interface{})
// 调用 Skipf 相当于在调用 Logf 之后调用 SkipNow
func (c *T) Skipf(format string, args ...interface{})
// Skipped 用于报告测试函数是否已被跳过
func (c *T) Skipped() bool
// 返回正在运行的测试或基准测试的名字
func (c *T) Name() string
// Parallel 用于表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试
func (t *T) Parallel()
// 执行名字为 name 的子测试 f ,并报告 f 在执行过程中是否出现了任何失败。Run 将一直阻塞直到 f 的所有并行测试执行完毕
// Run 可以在多个 goroutine 里面同时进行调用,但这些调用必须发生在 t 的外层测试函数(outer test function)返回之前
func (t *T) Run(name string, f func(t *T)) bool
⭐️ 示例代码
// demo/split.go
package demo
import "strings"
// Split 把字符串s按照给定的分隔符sep进行分割返回字符串切片
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
// demo/split_test.go,测试文件命令必须以 _test.go 为后缀
package demo
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) { // 测试函数名必须以 Test 开头,必须接收一个 *testing.T 类型参数
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
if !reflect.DeepEqual(got, want) { // 因为 slice 不能比较直接,借助反射包中的方法比较
t.Errorf("expected:%v, got:%v", want, got) // 测试失败输出错误提示
}
}
func TestSplitWithComplexSep(t *testing.T) {
got := Split("abcd", "bc")
want := []string{"a", "d"}
if !reflect.DeepEqual(got, want) {
t.Errorf("expected:%v, got:%v", want, got)
}
}
✏️ 运行结果
> go test
PASS
ok test/demo 0.474s
// -v 参数,会输出完整的测试结果
> go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok test/demo 0.452s
// -run 参数对应着一个正则表达式,只要函数名匹配的测试函数才会被执行
> go test -v -run=Sep
=== RUN TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok test/demo 0.461s
// -cover 参数,会输出测试覆盖率
> go test -cover
PASS
coverage: 100.0% of statements
ok test/demo 0.479s
t.Skip() 方法
// 为了节省时间支持在单元测试时跳过某些耗时的测试用例
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("short模式下会跳过该测试用例")
}
...
}
当执行go test -short
时就不会执行上面的TestTimeConsuming
测试用例。
t.Run() 方法
// 支持在测试函数中使用 t.Run() 方法执行一组测试用例,无需为不同的测试数据定义多个测试函数
func TestXxx(t *testing.T){
t.Run("case1", func(t *testing.T){...})
t.Run("case2", func(t *testing.T){...})
t.Run("case3", func(t *testing.T){...})
}
表格驱动测试
// demo/split_test.go,测试文件命令必须以 _test.go 为后缀
package demo
import (
"reflect"
"testing"
)
func TestSplitAll(t *testing.T) {
// 定义测试表格
// 这里使用匿名结构体定义了若干个测试用例,并为每个测试用例设置了一个名称
tests := []struct {
name string
input string
sep string
want []string
}{
{"base case", "a:b:c", ":", []string{"a", "b", "c"}},
{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
{"more sep", "abcd", "bc", []string{"a", "d"}},
{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
}
// 遍历测试用例
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // 使用 t.Run() 执行子测试
got := Split(tt.input, tt.sep)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("expected:%#v, got:%#v", tt.want, got)
}
})
}
}
go test -v
=== RUN TestSplitAll
=== RUN TestSplitAll/base_case
=== RUN TestSplitAll/wrong_sep
=== RUN TestSplitAll/more_sep
=== RUN TestSplitAll/leading_sep
--- PASS: TestSplitAll (0.00s)
--- PASS: TestSplitAll/base_case (0.00s)
--- PASS: TestSplitAll/wrong_sep (0.00s)
--- PASS: TestSplitAll/more_sep (0.00s)
--- PASS: TestSplitAll/leading_sep (0.00s)
PASS
ok test/demo 1.011s
t.Parallel() 方法
// demo/split_test.go,测试文件命令必须以 _test.go 为后缀
package demo
import (
"reflect"
"testing"
)
func TestSplitAll(t *testing.T) {
t.Parallel() // 将 TLog 标记为能够与其他测试并行运行
// 定义测试表格
// 这里使用匿名结构体定义了若干个测试用例
// 并且为每个测试用例设置了一个名称
tests := []struct {
name string
input string
sep string
want []string
}{
{"base case", "a:b:c", ":", []string{"a", "b", "c"}},
{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
{"more sep", "abcd", "bc", []string{"a", "d"}},
{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},
}
// 遍历测试用例
for _, tt := range tests {
tt := tt // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)
t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
t.Parallel() // 将每个测试用例标记为能够彼此并行运行
got := Split(tt.input, tt.sep)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("expected:%#v, got:%#v", tt.want, got)
}
})
}
}
> go test -v
=== RUN TestSplitAll
=== PAUSE TestSplitAll
=== CONT TestSplitAll
=== RUN TestSplitAll/base_case
=== PAUSE TestSplitAll/base_case
=== RUN TestSplitAll/wrong_sep
=== PAUSE TestSplitAll/wrong_sep
=== RUN TestSplitAll/more_sep
=== PAUSE TestSplitAll/more_sep
=== RUN TestSplitAll/leading_sep
=== PAUSE TestSplitAll/leading_sep
=== CONT TestSplitAll/base_case
=== CONT TestSplitAll/leading_sep
=== CONT TestSplitAll/more_sep
=== CONT TestSplitAll/wrong_sep
--- PASS: TestSplitAll (0.00s)
--- PASS: TestSplitAll/base_case (0.00s)
--- PASS: TestSplitAll/leading_sep (0.00s)
--- PASS: TestSplitAll/more_sep (0.00s)
--- PASS: TestSplitAll/wrong_sep (0.00s)
PASS
ok test/demo 0.485s
goconvey 使用
goconvey
是一个非常好用的 Go 测试框架,它直接与go test
集成,提供了很多丰富的断言函数,能够在终端输出可读的彩色测试结果,并且还支持全自动的Web UI
。
go get -u github.com/smartystreets/goconvey
断言函数
// 一般相等类
So(thing1, ShouldEqual, thing2)
So(thing1, ShouldNotEqual, thing2)
So(thing1, ShouldResemble, thing2) // a deep equals for arrays, slices, maps, and structs
So(thing1, ShouldNotResemble, thing2)
So(thing1, ShouldPointTo, thing2)
So(thing1, ShouldNotPointTo, thing2)
So(thing1, ShouldBeNil)
So(thing1, ShouldNotBeNil)
So(thing1, ShouldBeTrue)
So(thing1, ShouldBeFalse)
So(thing1, ShouldBeZeroValue)
// 数字数量比较类
So(1, ShouldBeGreaterThan, 0)
So(1, ShouldBeGreaterThanOrEqualTo, 0)
So(1, ShouldBeLessThan, 2)
So(1, ShouldBeLessThanOrEqualTo, 2)
So(1.1, ShouldBeBetween, .8, 1.2)
So(1.1, ShouldNotBeBetween, 2, 3)
So(1.1, ShouldBeBetweenOrEqual, .9, 1.1)
So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000)
So(1.0, ShouldAlmostEqual, 0.99999999, .0001) // tolerance is optional; default 0.0000000001
So(1.0, ShouldNotAlmostEqual, 0.9, .0001)
// 包含类
So([]int{2, 4, 6}, ShouldContain, 4)
So([]int{2, 4, 6}, ShouldNotContain, 5)
So(4, ShouldBeIn, ...[]int{2, 4, 6})
So(4, ShouldNotBeIn, ...[]int{1, 3, 5})
So([]int{}, ShouldBeEmpty)
So([]int{1}, ShouldNotBeEmpty)
So(map[string]string{"a": "b"}, ShouldContainKey, "a")
So(map[string]string{"a": "b"}, ShouldNotContainKey, "b")
So(map[string]string{"a": "b"}, ShouldNotBeEmpty)
So(map[string]string{}, ShouldBeEmpty)
So(map[string]string{"a": "b"}, ShouldHaveLength, 1) // supports map, slice, chan, and string
// 字符串类
So("asdf", ShouldStartWith, "as")
So("asdf", ShouldNotStartWith, "df")
So("asdf", ShouldEndWith, "df")
So("asdf", ShouldNotEndWith, "df")
So("asdf", ShouldContainSubstring, "sd") // optional 'expected occurences' arguments?
So("asdf", ShouldNotContainSubstring, "er")
So("adsf", ShouldBeBlank)
So("asdf", ShouldNotBeBlank)
// panic 类
So(func(), ShouldPanic)
So(func(), ShouldNotPanic)
So(func(), ShouldPanicWith, "") // or errors.New("something")
So(func(), ShouldNotPanicWith, "") // or errors.New("something")
// 类型检查类
So(1, ShouldHaveSameTypeAs, 0)
So(1, ShouldNotHaveSameTypeAs, "asdf")
// 时间和时间间隔类
So(time.Now(), ShouldHappenBefore, time.Now())
So(time.Now(), ShouldHappenOnOrBefore, time.Now())
So(time.Now(), ShouldHappenAfter, time.Now())
So(time.Now(), ShouldHappenOnOrAfter, time.Now())
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenWithin, duration, time.Now())
So(time.Now(), ShouldNotHappenWithin, duration, time.Now())
示例代码
package demo
import (
c "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestSplitAll(t *testing.T) {
c.Convey("基础用例", t, func() {
var (
s = "a:b:c"
sep = ":"
expect = []string{"a", "b", "c"}
)
got := Split(s, sep)
c.So(got, c.ShouldResemble, expect) // 断言
})
c.Convey("不包含分隔符用例", t, func() {
var (
s = "a:b:c"
sep = "|"
expect = []string{"a:b:c"}
)
got := Split(s, sep)
c.So(got, c.ShouldResemble, expect) // 断言
})
c.Convey("分隔符在开头或结尾用例", t, func() {
tt := []struct {
name string
s string
sep string
expect []string
}{
{"分隔符在开头", "*1*2*3", "*", []string{"", "1", "2", "3"}},
{"分隔符在结尾", "1+2+3+", "+", []string{"1", "2", "3", ""}},
}
for _, tc := range tt {
c.Convey(tc.name, func() { // 嵌套调用 Convey,只需要在顶层的 Convey 调用时传入 t
got := Split(tc.s, tc.sep)
c.So(got, c.ShouldResemble, tc.expect)
})
}
})
}
> go test -v
=== RUN TestSplitAll
基础用例 .
1 total assertion
不包含分隔符用例 .
2 total assertions
分隔符在开头或结尾用例
分隔符在开头 .
分隔符在结尾 .
4 total assertions
--- PASS: TestSplitAll (0.00s)
PASS
ok test/demo 0.497s
gomonkey 使用
gomonkey
是一个 Go 的打桩框架,提供了很多丰富的打桩函数/方法,旨在让用户在单元测试中低成本的完成打桩,将精力聚焦于业务功能的开发。
go get -u github.com/agiledragon/gomonkey
为函数打桩
// 为函数/接口打一个桩,target 表示函数名,double 表示桩函数
func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches
// 为函数/接口打一个特定的桩序列,target 表示函数名,outputs 表示桩序列参数(返回值序列)
func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
// Outputs 结构体中,Values 是一个可传递多个值切片,Times 指定返回的次数(值为 0 和 1 没有区别)
type Params []interface{}
type OutputCell struct {
Values Params
Times int
}
☕️ 示例代码
// 对 fake.Exec() 函数进行 mock
func Exec(cmd string, args ...string) (string, error) {
cmdPath, err := exec.LookPath(cmd)
if err != nil {
fmt.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
return "", errors.New("any")
}
var output []byte
output, err = exec.Command(cmdPath, args...).CombinedOutput()
if err != nil {
fmt.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
return "", errors.New("any")
}
fmt.Println("CMD[", cmdPath, "]ARGS[", args, "]OUT[", string(output), "]")
return string(output), nil
}
package demo
import (
"github.com/agiledragon/gomonkey"
"github.com/agiledragon/gomonkey/test/fake"
c "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestAll(t *testing.T) {
// 为函数/接口打一个桩
c.Convey("TestApplyFunc", t, func() {
patches := gomonkey.ApplyFunc(fake.Exec, func(cmd string, args ...string) (string, error) {
return "hello world", nil
})
defer patches.Reset() // 删除桩
output, err := fake.Exec("", "")
c.So(err, c.ShouldBeNil)
c.So(output, c.ShouldEqual, "hello world")
})
// 为函数/接口打一个特定的桩序列
c.Convey("TestApplyFuncSeq", t, func() {
patches := gomonkey.ApplyFuncSeq(fake.Exec, []gomonkey.OutputCell{
{Values: gomonkey.Params{"hello1", fake.ErrActual}, Times: 2}, // 指定第一次和第二次返回的序列
{Values: gomonkey.Params{"hello2", nil}}, // 指定第三次返回的序列
})
defer patches.Reset() // 删除桩
output1, err := fake.Exec("", "")
c.So(err, c.ShouldResemble, fake.ErrActual)
c.So(output1, c.ShouldEqual, "hello1")
output2, err := fake.Exec("", "")
c.So(err, c.ShouldResemble, fake.ErrActual)
c.So(output2, c.ShouldEqual, "hello1")
output3, err := fake.Exec("", "")
c.So(err, c.ShouldBeNil)
c.So(output3, c.ShouldEqual, "hello2")
})
}
为成员方法打桩
// 为成员方法打一个桩,target 表示对象类型,methodName 表示对象的方法名,double 表示桩函数
func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
// 为成员方法打一个特定的桩序列,target 表示对象类型,methodName 表示对象的方法名,outputs 表示桩序列参数(返回值序列)
func ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell) *Patches
func (this *Patches) ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell) *Patches
☕️ 示例代码
// 对 fake.Slice 类型的 Add() 方法进行 mock
type Slice []int
func NewSlice() Slice {
return make(Slice, 0)
}
func (this* Slice) Add(elem int) error {
for _, v := range *this {
if v == elem {
fmt.Printf("Slice: Add elem: %v already exist\n", elem)
return ErrElemExsit
}
}
*this = append(*this, elem)
fmt.Printf("Slice: Add elem: %v succ\n", elem)
return nil
}
func (this* Slice) Remove(elem int) error {
found := false
for i, v := range *this {
if v == elem {
if i == len(*this) - 1 {
*this = (*this)[:i]
} else {
*this = append((*this)[:i], (*this)[i+1:]...)
}
found = true
break
}
}
if !found {
fmt.Printf("Slice: Remove elem: %v not exist\n", elem)
return ErrElemNotExsit
}
fmt.Printf("Slice: Remove elem: %v succ\n", elem)
return nil
}
package demo
import (
"github.com/agiledragon/gomonkey"
"github.com/agiledragon/gomonkey/test/fake"
c "github.com/smartystreets/goconvey/convey"
"reflect"
"testing"
)
func TestAll(t *testing.T) {
// 为成员方法打一个桩
c.Convey("TestApplyMethod", t, func() {
patches := gomonkey.ApplyMethod(reflect.TypeOf(new(fake.Slice)), "Add", func(this *fake.Slice, elem int) error {
return nil
})
defer patches.Reset() // 删除桩
slice := fake.NewSlice()
err := slice.Add(1)
c.So(err, c.ShouldBeNil)
err = slice.Remove(1)
c.So(err, c.ShouldResemble, fake.ErrElemNotExsit)
})
// 为成员方法打一个特定的桩序列
c.Convey("TestApplyMenthodSeq", t, func() {
patches := gomonkey.ApplyMethodSeq(reflect.TypeOf(new(fake.Slice)), "Add", []gomonkey.OutputCell{
{Values: gomonkey.Params{nil}}, // 指定第一次返回的序列
{Values: gomonkey.Params{fake.ErrActual}, Times: 2}, // 指定第二次和第三次返回的序列
})
defer patches.Reset() // 删除桩
slice := fake.NewSlice()
err := slice.Add(1)
c.So(err, c.ShouldBeNil)
err = slice.Add(1)
c.So(err, c.ShouldResemble, fake.ErrActual)
err = slice.Add(1)
c.So(err, c.ShouldResemble, fake.ErrActual)
})
}
为函数变量打桩
// 为函数变量打一个桩,target 表示函数变量,double 表示桩函数
func ApplyFuncVar(target, double interface{}) *Patches
func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches
// 为函数变量打一个特定的桩序列,target 表示函数变量,outputs 表示桩序列参数(返回值序列)
func ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches
☕️ 示例代码
// 对 fake.Marshal 进行 mock
var Marshal = func(v interface{}) ([]byte, error) {
return nil, nil
}
package demo
import (
"github.com/agiledragon/gomonkey"
"github.com/agiledragon/gomonkey/test/fake"
c "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestAll(t *testing.T) {
// 为函数变量打一个桩
c.Convey("TestApplyFuncVal", t, func() {
patches := gomonkey.ApplyFuncVar(&fake.Marshal, func(v interface{}) ([]byte, error) {
return []byte("hello world"), nil
})
defer patches.Reset() // 删除桩
bytes, err := fake.Marshal(nil)
c.So(err, c.ShouldBeNil)
c.So(string(bytes), c.ShouldEqual, "hello world")
})
// 为函数变量打一个特定的桩序列
c.Convey("TestApplyFuncValSeq", t, func() {
patches := gomonkey.ApplyFuncVarSeq(&fake.Marshal, []gomonkey.OutputCell{
{Values: gomonkey.Params{[]byte("hello1"), nil}}, // 指定第一次返回的序列
{Values: gomonkey.Params{[]byte("hello2"), nil}, Times: 2}, // 指定第二次和第三次返回的序列
})
defer patches.Reset() // 删除桩
bytes, _ := fake.Marshal(nil)
c.So(string(bytes), c.ShouldEqual, "hello1")
bytes, _ = fake.Marshal(nil)
c.So(string(bytes), c.ShouldEqual, "hello2")
bytes, _ = fake.Marshal(nil)
c.So(string(bytes), c.ShouldEqual, "hello2")
})
}
为全局变量打桩
// 为全局变量打一个桩,target 表示全局变量,double 表示桩函数
func ApplyGlobalVar(target, double interface{}) *Patches
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches
☕️ 示例代码
package demo
import (
"github.com/agiledragon/gomonkey"
c "github.com/smartystreets/goconvey/convey"
"testing"
)
var num = 10
func TestAll(t *testing.T) {
// 为全局变量打一个桩
c.Convey("TestApplyFuncVal", t, func() {
patches := gomonkey.ApplyGlobalVar(&num, 100)
defer patches.Reset() // 删除桩
c.So(num, c.ShouldEqual, 100)
})
}