Go Convey测试框架入门
介绍
GoConvey是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性。
Golang虽然自带了单元测试功能,并且在GoConvey框架诞生之前也出现了许多第三方测试框架,但没有一个测试框架像GoConvey一样能够让程序员如此简洁优雅的编写测试代码。
官网:http://smartystreets.github.io/goconvey/
安装
go get 方式安装(go path时代,关闭go mod)
go
//下载源码并执行go build
//源码下载到$GOPATH/src目录
//go build之后的结果到$GOPATH/bin
go get github.com/smartystreets/goconvey
在$GOPATH/src目录下新增了github.com子目录,该子目录里包含了GoConvey框架的库代码 在$GOPATH/bin目录下新增了GoConvey框架的可执行程序goconvey
go install方式(开启go mod)
在gomod时代一般不需要先显式安装(gomod机制会自动从goproxy拉取依赖到本地cache),只需要go.mod中引入,然后go mod tidy即可。除非要使用GoConvey的web界面,这时需要提前安装GoConvey的二进制,命令为go install github.com/smartystreets/goconvey@latest。
go.mod:
go
module ziyi.convey.com
go 1.19
require github.com/smartystreets/goconvey v1.8.1
require (
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/smarty/assertions v1.15.0 // indirect
)
go
//我后面要演示web页面,因此这里显示安装一下
//安装之后,依然会将编译好的二进制放在$GOPATH/bin
go install github.com/smartystreets/goconvey@latest
我的GOPATH为E:\Go\GoPro:
使用
注意事项
- import goconvey包时,前面加点号".",以减少冗余的代码。凡是在测试代码中看到Convey和So两个方法,肯定是convey包的。我们需要注意不要在产品代码中定义相同的函数名
- 测试函数的名字必须以Test开头,而且参数类型必须为*testing.T
- 每个测试用例必须使用Convey函数包裹起来,它的第一个参数为string类型的测试描述,第二个参数为测试函数的入参(类型为*testing.T),第三个参数为不接收任何参数也不返回任何值的函数(习惯使用闭包)
- Convey函数的第三个参数闭包的实现中通过So函数完成断言判断,它的第一个参数为实际值,第二个参数为断言函数变量,第三个参数或者没有(当第二个参数为类ShouldBeTrue形式的函数变量)或者有(当第二个函数为类ShouldEqual形式的函数变量)
案例一:1个测试用例1个Convey
c_test.go:
go
package main
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestEqualWithSingleTestCase(t *testing.T) {
// test name:用例名称
// t:需要传入*testing.T
// func(){} 测试函数
Convey("test name", t, func() {
//1+1:断言
//ShouldEqual:convey内置的断言
//2:期望结果
So(1+1, ShouldEqual, 2)
})
}
案例二:多个Convey多个测试用例
①平铺写法
go
func TestEqualWithMultipleTestCase(t *testing.T) {
Convey("test add case", t, func() {
So(1+1, ShouldEqual, 2)
})
Convey("test sub case", t, func() {
So(1-1, ShouldEqual, 0)
})
Convey("test multi case", t, func() {
So(1*1, ShouldNotEqual, -1)
})
}
②Convey嵌套写法
只需要最外层的Convey传t *testing.T即可
go
func TestEqualWithMultipleTestCaseAndNested(t *testing.T) {
Convey("test case", t, func() {
Convey("test add case", func() {
So(1+1, ShouldEqual, 2)
})
Convey("test sub case", func() {
So(1-1, ShouldEqual, 0)
})
Convey("test multi case", func() {
So(1*1, ShouldNotEqual, -1)
})
})
}
案例三:"函数式"断言(assert传入函数)
go
// 案例三:函数式断言
func TestFunctionalAssertion(t *testing.T) {
Convey("test case", t, func() {
So(add(1, 1), ShouldEqual, 2)
})
}
func add(a, b int) int {
return a + b
}
案例四:忽略部分Convey断言
针对想忽略但又不想删掉或注释掉某些断言操作,GoConvey提供了Convey/So的Skip方法:
- SkipConvey函数表明相应的闭包函数将不被执行
- SkipSo函数表明相应的断言将不被执行
当存在SkipConvey或SkipSo时,测试日志中会显式打上"skipped"形式的标记:
当测试代码中存在SkipConvey时,相应闭包函数中不管是否为SkipSo,都将被忽略。
不管存在SkipConvey还是SkipSo时,测试日志中都有字符串"{n} total assertions (one or more sections skipped)",其中{n}表示测试中实际已运行的断言语句数
go
// 案例四:忽略Convey断言
//忽略所有断言
func TestCaseSkipConvey(t *testing.T) {
SkipConvey("test case", t, func() {
So(add(1, 1), ShouldEqual, 2)
})
}
//忽略某些断言(SkipSo的断言将被忽略)
func TestCaseSkipSo(t *testing.T) {
Convey("test case", t, func() {
SkipSo(add(1, 1), ShouldEqual, 2)
So(1-1, ShouldEqual, 0)
})
}
比如我执行TestCaseSkipSo函数:
案例五:定制断言函数
①原理分析
- 查看So源码
go
func So(actual interface{}, assert Assertion, expected ...any)
- 点击查看Assertion结构体
go
type Assertion func(actual any, expected ...any) string
当Assertion的变量的返回值为""时表示断言成功,否则表示失败:
go
const assertionSuccess = ""
- 结论
因此我们只需要按照结构,实现func,最后返回空串代表断言成功,否则失败
②实际操作
从上面的分析我们可以知道,当Assertion最后返回""代表断言成功,反之失败
go
// 案例五:定制断言
func TestCustomAssertion(t *testing.T) {
Convey("test custom assert", t, func() {
So(1+1, CustomAssertionWithRaiseMoney, 2)
})
}
func CustomAssertionWithRaiseMoney(actual any, expected ...any) string {
if actual == expected[0] {
return ""
} else {
return "doesn't raise money"
}
}
案例六:访问web页面
如果要访问web页面,需要有goconvey.exe程序并运行,同时需要将xxx_test.go文件放在与exe同目录,否则无法识别到
- go get "github.com/smartystreets/goconvey"
- go install "github.com/smartystreets/goconvey" (如果开启了go mod,则用该方式)
默认安装到$GOPATH/bin目录下
执行exe后,访问本地localhost:8080端口即可看到web页面,页面展示的了单测的通过情况等。
拓展:gomonkey(打桩工具)
如果在被测函数中调用了其他函数(比如其他业务方的),可以使用以下方法,gomonkey打桩工具
go
//安装依赖
go get "github.com/agiledragon/gomonkey"
给全局变量打桩
使用 gomonkey 可以方便地模拟函数的行为
go
// 拓展:配合monkey打桩
var num = 10 //全局变量
func TestApplyGlobalVar(t *testing.T) {
Convey("TestApplyGlobalVar", t, func() {
Convey("change", func() {
//模拟函数行为,给全局变量复制,在函数结束后直接通过reset恢复全局变量值
patches := gomonkey.ApplyGlobalVar(&num, 150)
defer patches.Reset()
So(num, ShouldEqual, 150)
})
Convey("recover", func() {
So(num, ShouldEqual, 10)
})
})
}
给函数打桩
go
//对函数进行打桩
func TestFunc(t *testing.T) {
// mock 了 networkCompute(),返回了计算结果2
patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
return 2, nil
})
defer patches.Reset()
sum, err := Compute(1, 2)
println("expected %v, got %v", 2, sum)
if sum != 2 || err != nil {
t.Errorf("expected %v, got %v", 2, sum)
}
}
func networkCompute(a, b int) (int, error) {
// do something in remote computer
c := a + b
return c, nil
}
func Compute(a, b int) (int, error) {
sum, err := networkCompute(a, b)
return sum, err
}
全部代码:
欢迎大家star
- Github:
https://github.com/ziyifast/ziyifast-code_instruction/tree/main/go-demo/go-convey
c_test.go:
go
package main
import (
"github.com/agiledragon/gomonkey"
_ "github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
// 案例一:一个Convey,一个用例
func TestEqualWithSingleTestCase(t *testing.T) {
// test name:用例名称
// t:需要传入*testing.T
// func(){} 测试函数
Convey("test name", t, func() {
//1+1:断言
//ShouldEqual:convey内置的断言
//2:期望结果
So(1+1, ShouldEqual, 2)
})
}
// 案例二:多个Convey,多个用例(平铺写法)
func TestEqualWithMultipleTestCase(t *testing.T) {
Convey("test add case", t, func() {
So(1+1, ShouldEqual, 2)
})
Convey("test sub case", t, func() {
So(1-1, ShouldEqual, 0)
})
Convey("test multi case", t, func() {
So(1*1, ShouldNotEqual, -1)
})
}
// 案例二:多个Convey,多个用例(嵌套写法)
func TestEqualWithMultipleTestCaseAndNested(t *testing.T) {
Convey("test case", t, func() {
Convey("test add case", func() {
So(1+1, ShouldEqual, 2)
})
Convey("test sub case", func() {
So(1-1, ShouldEqual, 0)
})
Convey("test multi case", func() {
So(1*1, ShouldNotEqual, -1)
})
})
}
// 案例三:函数式断言
func TestFunctionalAssertion(t *testing.T) {
Convey("test case", t, func() {
So(add(1, 1), ShouldEqual, 2)
})
}
func add(a, b int) int {
return a + b
}
// 案例四:忽略Convey断言
// 忽略所有断言
func TestCaseSkipConvey(t *testing.T) {
SkipConvey("test case", t, func() {
So(add(1, 1), ShouldEqual, 2)
})
}
// 忽略某些断言(SkipSo的断言将被忽略)
func TestCaseSkipSo(t *testing.T) {
Convey("test case", t, func() {
SkipSo(add(1, 1), ShouldEqual, 2)
So(1-1, ShouldEqual, 0)
})
}
// 案例五:定制断言
func TestCustomAssertion(t *testing.T) {
Convey("test custom assert", t, func() {
So(1+1, CustomAssertionWithRaiseMoney, 2)
})
}
func CustomAssertionWithRaiseMoney(actual any, expected ...any) string {
if actual == expected[0] {
return ""
} else {
return "doesn't raise money"
}
}
// 拓展:配合monkey打桩
var num = 10 //全局变量
func TestApplyGlobalVar(t *testing.T) {
Convey("TestApplyGlobalVar", t, func() {
Convey("change", func() {
//模拟函数行为,给全局变量复制,在函数结束后直接通过reset恢复全局变量值
patches := gomonkey.ApplyGlobalVar(&num, 150)
defer patches.Reset()
So(num, ShouldEqual, 150)
})
Convey("recover", func() {
So(num, ShouldEqual, 10)
})
})
}
// 对函数进行打桩
func TestFunc(t *testing.T) {
// mock 了 networkCompute(),返回了计算结果2
patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
return 2, nil
})
defer patches.Reset()
sum, err := Compute(1, 2)
println("expected %v, got %v", 2, sum)
if sum != 2 || err != nil {
t.Errorf("expected %v, got %v", 2, sum)
}
}
func networkCompute(a, b int) (int, error) {
// do something in remote computer
c := a + b
return c, nil
}
func Compute(a, b int) (int, error) {
sum, err := networkCompute(a, b)
return sum, err
}
go.mod:
go
module ziyi.convey.com
go 1.19
require (
github.com/agiledragon/gomonkey v2.0.2+incompatible
github.com/smartystreets/goconvey v1.8.1
)
require (
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/smarty/assertions v1.15.0 // indirect
)
参考文章: