Go 单元测试从 0 到 1
文章目录
- 1.什么是单元测试
- 2.单元测试的作用
- 3.Go 如何写单元测试
- 4.go test 命令
- 4.1 简介
- 4.2 示例
- 5.快速生成单测代码
- 6.看看单元测试覆盖率
- 7.使用单测框架写单测
- 7.1 testify + gomonkey
- 7.2 goconvey + gomock
- 7.2.1 goconvey
- 7.2.2 gomock
- 8.再谈 mock
- 8.1 test doubles
- 8.2 mock 与 stub
- 8.3 不要滥用 mock
- 9.小结
- 参考文献
1.什么是单元测试
单元测试(Unit Testing),是指对软件中的最小可测试单元进行检查和验证。
对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小被测功能模块。
在 Go 中,一般指对函数的单元测试。
2.单元测试的作用
单元测试可以检查我们的代码能否按照预期执行,来提升代码质量。
通过单元测试,我们可以设置多个测试用例,执行要测试的函数,判断是否符合预期。尽可能达保证函数功能没有问题,或者出现我们预知的错误。一次书写测试用例,随着代码一起永久保留,来验证函数功能,这就是单元测试的好处。
3.Go 如何写单元测试
Go 本身对自动化测试非常友好,并且有许多优秀的第三方测试框架,非常好上手。
首先看一下 Go 官方的 testing 包。
要编写一个测试文件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数。将该文件放在与被测试文件相同的包中,该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。
测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值,函数名必须以 Test 开头,建议后跟要测试的函数名。
记得一定要先看一下 Package testing 的官方文档!
下面利用 Go 官方 testing 包给出一个示例。
代码目录结构:
gotest- go.mod- go.sum- main.go- hello- hello.go- hello_test.go
main.go:
package mainimport ("main/hello"
)func main() {fmt.Println(hello.Hello())
}
hello.go:
package hellofunc Hello() string {return "Hello world"
}
我们新建一个单测文件 hello_test.go,为函数 Hello() 添加单元测试。
hello_test.go:
package helloimport "testing"func TestHello(t *testing.T) {got := Hello()want := "Hello world"if got != want {t.Errorf("got %q want %q", got, want)}
}
进入 hello 目录执行命令go test
,或指定目录 go test ./hello
,所有以_test.go
结尾的源码文件内以 Test 开头的函数会自动被执行。
输出:
PASS
ok main/hello 0.173s
我们也可以加上-v
选项,输出单测执行的详细过程:
go test -v
=== RUN TestHello
--- PASS: TestHello (0.00s)
PASS
ok main/hello 0.176s
该结果,表示单测通过了,返回的值与我们预期的值是相同的。
现在尝试把预期结果修改一下:
want := "Hello fuck"
测试结果:
D:\code\gotest\hello>go test -v
=== RUN TestHellohello_test.go:9: got "Hello world" want "Hello fuck"
--- FAIL: TestHello (0.00s)
FAIL
exit status 1
FAIL main/hello 0.127s
此时提示测试不通过,得到的值与预期的值不相同。
至此,利用 Go 官方包 testing 便完成了一个简单的单元测试,我们可以进行正确或错误的测试。
4.go test 命令
4.1 简介
go test 是 Go 用来执行测试函数(test function)、基准函数(benchmark function)和示例函数(example function)的命令。
行 go test 命令,它会在 *_test.go 文件中寻找 test、benchmark 和 example 函数来执行。测试函数名必须以 TestXXX 开头,基准函数名必须以 BenchmarkXXX 开头,示例函数必须以 ExampleXXX 开头。
// test 测试函数
func TestXXX(t *testing.T) { ... }// benchmark 基准函数
func BenchmarkXXX(b *testing.B) { ... }// examples 示例函数,其相关命名方式可以查看第一篇文章
func ExamplePrintln() {Println("The output of\nthis example.")// Output: The output of// this example.
}
关于更多测试函数的信息请查看go help testfunc
。
go test 有很多选项,主要分为三类:一类是控制构建,一类是控制测试行为,一类是用于状态分析。
这里只列出常用的用于控制测试行为的选项:
-bench regexp只执行匹配对应正则表达式的 benchmark 函数,如执行所有性能测试 "-bench ." 或 "-bench=."
-benchtime t对每个 benchmark 函数运行指定时间。如 -benchtime 1h30,默认值为 1s。特殊语法 Nx 表示运行基准测试 N 次(如 -benchtime 100x)
-run regexp只运行匹配对应正则表达式的 test 和 example 函数,例如 "-run Array" 那么就执行函数名包含 Array 的单测函数
-cover开启测试覆盖率
-v显示测试的详细命令
详见官方文档 Testing flags。
4.2 示例
假设在文件 add.go 有一个被测试函数
package hellofunc Add(a, b int) int {return a + b
}
- 测试函数(test function)
在测试文件 add_test.go 添加一个单元测试函数 TestAdd:
package hellofunc TestAdd(t *testing.T) {sum := Add(5, 5)if sum == 10 {t.Log("the result is ok")} else {t.Fatal("the result is wrong")}
}
比如使用 -run 来运行指定单元测试函数,发现只运行了 TestAdd 测试函数。
go test -v -run TestAdd main/hello
=== RUN TestAddadd_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
ok main/hello 0.170s
- 基准函数(benchmark function)
添加一个性能测试函数 BenchmarkAdd:
package hellofunc BenchmarkAdd(b *testing.B) {for n := 0; n < b.N; n++ {Add(1, 2)}
}
运行指定基准函数:
go test -bench BenchmarkAdd main/hello
goos: windows
goarch: amd64
pkg: main/contain
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkAdd-8 1000000000 0.2333 ns/op
PASS
ok main/contain 0.586s
- 示例函数(example function)
package hellofunc ExampleAdd() {fmt.Println(Add(1, 2))// Output: 3
}
运行指定示例函数:
go test -v -run ExampleAdd main/contain
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS
ok main/contain (cached)
注意: 示例函数类似于测试函数,但不是使用 *testing.T 来报告成功或失败,而是将输出打印到 os.Stdout。如果示例函数中的最后一条注释以“Output:”开头,则将输出与注释进行精确比较(参见上面的示例)。如果最后一条注释以“Unordered output:”开头,则将输出与注释进行比较,但忽略行的顺序。编译了一个没有此类注释的示例函数,会被编译但不会被执行。如果在“Output:”之后没有文本,示例函数仍会被编译并执行,并且预期不会产生任何输出。
5.快速生成单测代码
实际上,不同函数的单测代码虽然逻辑不同,但结构是一样的,长得非常相似,因此重复的代码可以使用工具来生成,不用手动繁琐地重复书写。
常用的 IDE,比如 GoLand 或 VS Code,都自带了生成单元测试代码的工具。以 GoLand 为例,可以快速为函数、文件或包生成测试代码。在源码文件中”右键函数名 > Generate… > Test for function“ 便可以快速生成对应函数的单测代码模板,然后我们在生成的模板代码中添加具体的测试用例即可。
使用该方法,为上面的 Hello() 函数生成的测试代码如下:
func TestHello(t *testing.T) {tests := []struct {name stringwant string}{// TODO: Add test cases.}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := Hello(); got != tt.want {t.Errorf("Hello() = %v, want %v", got, tt.want)}})}
}
我们在注释处添加测试用例即可,以表驱动的方式完成测试用例的书写与执行,非常快捷方便。
6.看看单元测试覆盖率
写好测试后,可以利用 Go 自带的工具 test coverage 查看一下单元测试覆盖率。
测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码得到执行。 如果执行测试函数导致 80%的语句得到了运行,则测试覆盖率为 80%。
我们来试一下。
D:\code\gotest>go test -v -cover ./hello
=== RUN TestHello
--- PASS: TestHello (0.00s)
=== RUN TestAddhello_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
coverage: 100.0% of statements
ok main/hello 0.154s coverage: 100.0% of statements
可以看到,目录 hello 下的所有单测都通过了,且报告覆盖率为 100%.
7.使用单测框架写单测
学会使用 Go 官方 testing 包写单元测试是远远不够的,因为在实际项目开发中,面对复杂的逻辑判断,繁多的测试用例,网络IO调用等,都加大了单测编写与管理的难度,此时我们需要用到更好的测试框架来完成单测的书写。
7.1 testify + gomonkey
这里推荐使用 testify + gomonkey 开源库来完成 Go 单元测试的书写。
testify 提供了三个能力:
- 断言能力:assert 和 require
- 单测组织:suite
- mock
其中 mock 的功能,但我们不使用,因为有更好用的 mock 库。
suite 提供了几个接口,可以用来做初始化和释放的动作
方法 | 说明 |
---|---|
SetupSuite | 在整个suite开始时执行一次 |
SetupTest | 在每个测试函数开始之前执行 |
BeforeTest | 在每个测试函数开始之前执行,类似SetupTest,带suite和测试函数名 |
AfterTest | 在每个测试函数结束之后执行,类似TearDownTest,带suite和测试函数名 |
TearDownTest | 在每个测试函数结束之后执行 |
TearDownSuite | 在整个suite结束时执行一次 |
gomonkey 主要用于提供 mock 能力。
说到 mock,其本意是模拟,就是对一些不想执行的函数,比如有网络 IO 或对 DB 有写入的函数,因为测试环境网络不通或不想执行单测而向 DB 写入数据,都可以将其 mock 住,写一个替代函数。执行单测的时候会调用这个替代函数,相当于替代函数模拟了原函数。
下面使用 testify + gomonkey 给出使用示例。
先改造一下 Hello() 和 Add() 函数。
// Hello 返回 Hello world 和写入 DB 的值
func Hello(a, b int) string {return fmt.Sprintf("Hello world and %v", Add(a, b))
}// Add 将 a b 和写入 DB 并返回
func Add(a, b int) int {sum := a + b// 省略的 DB 操作return sum
}
我们为 Hello() 函数添加单元测试并 mock 住 Add() 函数。
package helloimport ("testing""github.com/agiledragon/gomonkey""github.com/stretchr/testify/assert""github.com/stretchr/testify/suite"
)// TestSuiteHello 测试套件
type TestSuiteHello struct {suite.Suitepatches []*gomonkey.Patches
}// SetupTest 测试套件初始化
func (suite *TestSuiteHello) SetupTest() {// mock Add 函数p := gomonkey.ApplyFunc(Add, func(a, b int) int {// 不执行DB写入,只返回加和结果return a + b})suite.patches = append(suite.patches, p)
}// TearDownSuite 测试套件析构
func (suite *TestSuiteHello) TearDownSuite() {for _, p := range suite.patches {p.Reset()}
}// TestHelloSuite 启动测试套件
func TestHelloSuite(t *testing.T) {suite.Run(t, new(TestSuiteHello))
}// TestHello hello 函数单测
func (suite *TestSuiteHello) TestHello() {type args struct {a intb int}tests := []struct {name stringargs argswant string}{{"testcase1", args{1, 2}, "Hello world and 3"},{"testcase2", args{3, 4}, "Hello world and 7"},{"testcase3", args{5, 6}, "Hello world and 11"},}for _, t := range tests {res := Hello(t.args.a, t.args.b)assert.Equal(suite.T(), t.want, res, t.name)}
}
自定义的测试套件 TestSuiteHello,可以添加多个以 Test 开头的单测,将被一一执行。
执行单元测试:
D:\code\gotest>go test -v ./hello
=== RUN TestHelloSuite
=== RUN TestHelloSuite/TestHello
--- PASS: TestHelloSuite (0.00s)--- PASS: TestHelloSuite/TestHello (0.00s)
PASS
ok main/hello 0.149s
测试通过。
7.2 goconvey + gomock
7.2.1 goconvey
如果觉得上面使用表驱动的方式来管理测试用例太过简单,不够丰富,那么可以使用单测框架 goconvey 和 Go 官方提供的 mock 框架 gomock 来完成单测的开发。
goconvey 的单测组织形式更富逻辑性和结构化,更有可读性和可维护性。此外还具有丰富的断言函数,Web 界面,是极其常用的测试框架。
goconvey 的断言以 So 函数包裹,常用的断言有 ShouldEqual,ShouldBeNil,ShouldResemble(就是递归比较,类似 reflect.DeepEqual)。自定义断言也比较方便,实现 assertion 函数就可以。
goconvey 还可以无限嵌套,将相关的用例集合在一起,类似于:
import("testing". "github.com/smartystreets/goconvey/convey"
)func TestFoo(t *testing.T) {Convey("测试用例 1", t, func() {Convey("测试用例 1.1", func() { //第 2 层及其以上不需要传递 testing.T 变量// ...})Convey("测试用例 1.2", func() {// ...})}
}
使用 goconvey 改写上面 TestHello()。
package helloimport ("testing""github.com/smartystreets/goconvey/convey"
)func TestHello(t *testing.T) {convey.Convey("test case 1", t, func() {s := Hello(1, 2)convey.So(s, convey.ShouldEqual, "Hello world and 3")})convey.Convey("test case 2", t, func() {s := Hello(3, 4)convey.So(s, convey.ShouldEqual, "Hello world and 7")})convey.Convey("test case 3", t, func() {s := Hello(5, 6)convey.So(s, convey.ShouldEqual, "Hello world and 11")})
}
提一嘴,goconvey 的作者又搞了一个新的框架 gunit,相关文章有 A History of Testing in Go at SmartyStreets 及其译文。gunit 风格更像 testify,不过 star 数不到 1K,感兴趣的同学可以了解一下。
7.2.2 gomock
gomock 是 Go 官方提供的 mock 框架,同时还提供了 mockgen 工具用来辅助生成测试代码。
其特点为:
- 基于接口
- 能够与 Golang 内置的 testing 包良好集成
何为基于接口,一开始不太好理解它的用法,先来看看一段业务代码。
代码结构:
- main.go
- db- db.go
//
// db.go
//package dbimport ("log"
)// OrderDBI 定义了一个订单接口,有一个获取名称的方法
type OrderDBI interface {GetName(orderid int) string
}// OrderInfo 定义结构体
type OrderInfo struct {orderid int
}// GetName 实现接口的方法 GetName
func (order OrderInfo) GetName(orderid int) string {log.Println("原本应该连接数据库去取名称")return "foo"
}//
// main.go
//package mainimport ("fmt""main/db"
)func main() {// 创建接口实例var orderDBI db.OrderDBIorderDBI = new(db.OrderInfo)// 调用方法,返回名称ret := orderDBI.GetName(1)fmt.Println("取到的用户名:", ret)
}
运行这段代码可以得到“取到的用户名:foo”。
假设这个 GetName 是需要连接数据库去取用户名,那我们想针对 GetName 写单测的话,就要真实连接一个数据库才行,意味着在任何一台电脑上想运行单测代码都必须要依赖数据库。
此时我们可以使用 gomock 将其 mock 住,提前定义好返回内容。
首先,我们需要预先将 mockgen 安装好,可以通过以下命令安装:
go install github.com/golang/mock/mockgen
安装好后,我们使用 mockgen 来 mock 前面的 Add() 函数。
mockgen -source=./db/db.go -destination=./db/db_mock.go -package=db
-source 需要 mock 的源文件
-destination 生成的 mock 文件路径
-package 所属包
务必开启 Modules 管理,不然使用 mockgen 可能会出现这样的提示Loading input failed: Source directory is outside GOPATH
。
命令执行成功后会生成指定的 mock 文件,点进去查阅一下,有一个EXPECT()
需要重点留意一下。
// Code generated by MockGen. DO NOT EDIT.
// Source: ./db/db.go// Package db is a generated GoMock package.
package dbimport (reflect "reflect"gomock "github.com/golang/mock/gomock"
)// MockOrderDBI is a mock of OrderDBI interface.
type MockOrderDBI struct {ctrl *gomock.Controllerrecorder *MockOrderDBIMockRecorder
}// MockOrderDBIMockRecorder is the mock recorder for MockOrderDBI.
type MockOrderDBIMockRecorder struct {mock *MockOrderDBI
}// NewMockOrderDBI creates a new mock instance.
func NewMockOrderDBI(ctrl *gomock.Controller) *MockOrderDBI {mock := &MockOrderDBI{ctrl: ctrl}mock.recorder = &MockOrderDBIMockRecorder{mock}return mock
}// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOrderDBI) EXPECT() *MockOrderDBIMockRecorder {return m.recorder
}// GetName mocks base method.
func (m *MockOrderDBI) GetName(orderid int) string {m.ctrl.T.Helper()ret := m.ctrl.Call(m, "GetName", orderid)ret0, _ := ret[0].(string)return ret0
}// GetName indicates an expected call of GetName.
func (mr *MockOrderDBIMockRecorder) GetName(orderid interface{}) *gomock.Call {mr.mock.ctrl.T.Helper()return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockOrderDBI)(nil).GetName), orderid)
}
MockOrderDBIMockRecorder 和 MockOrderDBI 都实现了 interface 的方法。前者用来记录预期的行为,后者匹配并执行预期的行为。
下面是一个 mock 的执行流程。
gomock 的核心结构就是 Call,expectedCalls 是 call 的集合。
Call 对象有一系列的方法来表示预期的行为。
调用方法 | 说明 |
---|---|
After(preReq Call) *Call | 指定执行顺序 |
AnyTimes() *Call | 允许调用次数为 0 次或更多次 |
Do(f interface{}) *Call | 指定匹配时执行的操作 |
Return(rets …interface{}) *Call | 模拟返回值 |
DoAndReturn(f interface{}) *Call | 指定匹配是执行的操作,并且模拟返回值 |
MaxTimes(n int) *Call | 匹配到的方法最多可以被调用次数n 次 |
MinTimes(n int) *Call | 匹配到的方法最少要被调用次数n 次 |
SetArg(n int, value interface{}) *Call | 设置第n个参数的值为value |
String() String | 返回其字符串形式 |
Times(n int) *Call | 匹配到的方法必须被调用次数 n 次 |
另外一个就是 Call 的入参匹配规则。
匹配方法 | 说明 |
---|---|
Any() Matcher | 始终匹配 |
AssignableToTypeOf(x interface{}) Matcher | 如果入参可赋值给目标,则匹配,使用的是reflect.Type的AssignableTo |
Eq(x interface{}) Matcher | 相等则匹配 |
Nil() Matcher | 为 nil 则匹配 |
Not(x interface{}) Matcher | 匹配条件取反 |
接下来新建一个 db_test.go,给出一个使用示例。
func TestGetName(t *testing.T) {// 新建一个 mockControllerctrl := gomock.NewController(t)// 断言 DB.GetName() 方法是否被调用defer ctrl.Finish()// mock 接口mock := NewMockOrderDBI(ctrl)// 模拟传入值与预期的返回值mock.EXPECT().GetName(gomock.Eq(1225)).Return("foo")//前面定义了传入值与返回值//在这里if v := mock.GetName(1225); v != "foo" {t.Fatal("expected foo, but got", v)}else{log.Println("通过 mock 取到的 name:", v)}
}
执行上面的单测输出:
go test -v ./db
=== RUN TestGetName
2022/02/05 14:24:58 通过mock取到的name: foo
--- PASS: TestGetName (0.00s)
PASS
ok main/db 0.386s
可以看到测试通过了,与我们预期的值相符。是不是突然 Get 到了 GoMock 的用法~
8.再谈 mock
我们对不需要或无法执行单测时运行的函数将其 mock 住,模拟返回一个假值。谈到 mock,我们有必要说一下与 mock 相关的概念。。
8.1 test doubles
在《xUnit Test Patterns》一书中,作者首次提出 test doubles(测试替身)的概念。我们常挂在嘴边的 mock 只是其中一种,而且是最容易与 stub(打桩)混淆的一种。在前文中对 gomonkey 的介绍,你可以注意到了,我没有使用 mock,全部是 stub。是的,gomonkey 不是mock工具,只是一个高级打桩的工具,适配了我们大部分的使用场景。
测试替身,共有五种:可以参考这篇翻译《xUnit Test Patterns》学习笔记6 - Test Double 。
Dummy Object
用于传递给调用者但是永远不会被真实使用的对象,通常它们只是用来填满参数列表Test Stub
Stubs通常用于在测试中提供封装好的响应,譬如有时候编程设定的并不会对所有的调用都进行响应。Stubs也会记录下调用的记录,譬如一个email gateway就是一个很好的例子,它可以用来记录所有发送的信息或者它发送的信息的数目。简而言之,Stubs一般是对一个真实对象的封装Test Spy
Test Spy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirect outputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,Test Spy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性Mock Object
针对设定好的调用方法与需要响应的参数封装出合适的对象Fake Object
Fake 对象常常与类的实现一起起作用,但是只是为了让其他程序能够正常运行,譬如内存数据库就是一个很好的例子。
8.2 mock 与 stub
mock 和 stub 应该是最容易混淆的,而且习惯上我们统一用 mock 去形容模拟返回的能力,习惯成自然,也就把 mock 常挂在嘴边了。
就我的理解,stub 可以理解为 mock 的子集,mock 更强大一些:
- mock 可以验证实现过程,验证某个函数是否被执行,被执行几次
- mock可以依条件生效,比如传入特定参数,才会使mock效果生效
- mock可以指定返回结果
- 当mock指定任何参数都返回固定的结果时,它等于stub
只不过,Go 的 mock 工具 gomock 只基于接口生效,不适合大部分项目,而 gomonkey 的 stub 覆盖了大部分的使用场景。
关于 mock 和 stub 的资料还可参考 Mocks Aren’t Stubs及其译文。
关于 mock 框架,这里给出一个简单的对比说明。
框架 | 类型 | 特点 | 资料 |
---|---|---|---|
gomock | stub |
1.由Golang官方开发维护的测试框架,实现了较为完整的基于 interface 的 mock 功能; 2.提供相应工具(mockgen)生成单测代码提高单测书写效率。 |
golang/mock |
gomokey | stub |
1. 支持对于函数、方法、接口、局部变量、全局变量、调用序列等插桩; 2. 不支持异包私有函数 mock。 |
Monkey Patching in Go gomonkey底层原理剖析.pdf |
8.3 不要滥用 mock
mock 虽好,切勿贪杯。
两个门派。
约从 2004-2005 年间,江湖上形成两大门派:经典测试驱动开发派 和 mockist(mock极端派)。
先说 mockist。他主张将被测函数 所有 调用的外面函数,全部 mock。也即,只关注被测函数自己的一行行代码,只要调用其他函数,全都 mock 掉,用假数据来测试。
再说经典测试驱动开发派,他们主张不要滥用 mock,能不 mock 就不 mock,被测单元也不一定是具体的一个函数,可能是多个函数,串起来。必要的时候再 mock。
两个门派相争多年,理论各有利弊,至今仍然共存。存在即合理。比如 mockist,使用了过多的 mock,无法覆盖函数接口,这部分又是很容易出错的;经典派,串的太多,又被质疑是集成测试。
对于我们实际应用,不必强制遵从某一派,结合即可,需要的时候 mock,尽量少 mock,不用纠结。
什么时候适合 mock?
如果一个对象具有以下特征,比较适合使用 mock 对象:
- 该对象提供非确定的结果(比如当前的时间或者当前的温度)
- 对象的某些状态难以创建或者重现(比如网络错误或者文件读写错误)
- 对象方法上的执行太慢(比如在测试开始之前初始化数据库)
- 该对象还不存在或者其行为可能发生变化(比如测试驱动开发中驱动创建新的类)
- 该对象必须包含一些专门为测试准备的数据或者方法(后者不适用于静态类型的语言,流行的 mock 框架不能为对象添加新的方法,stub 是可以的)。
因此,不要滥用 mock(stub),当被测方法中调用其他方法函数,第一反应应该走进去串起来,而不是从根部就 mock掉了。
关于 mock 的使用问题,推荐阅读肖鹏的《mock七宗罪》。
9.小结
关于单元测试,本文从 0 到 1 讲解了 Go 如何编写测试用例,熟练掌握 Golang 中单元测试的书写是一位合格 gopher 的必备技能。
使用单测框架编写单测,如 testify + gomonkey 或 goconvey + gomock 均可,可根据实际场景选择使用。关于其他的单测框架,感兴趣的你可自行了解,比如 mock 框架还有 monkey 和 gostub。
参考文献
GoLang快速上手单元测试(思想、框架、实践)
gomonkey 1.0 正式发布!
Package testing 中文文档
《xUnit Test Patterns》学习笔记6 - Test Double
Mocks Aren’t Stubs
Mock并非Stub(翻译)
Golang go 命令
Go 单元测试从 0 到 1相关推荐
- c语言 单元测试工具 免费下载,雨田单元测试系统(C语言单元测试) 1.0官方版
雨田单元测试系统(C语言单元测试)是一款适用于C语言文件的单元测试和集成测试系统.可以大幅度提高对C语言测试效率,从而提供软件质量. 雨田单元测试系统介绍 雨田单元测试系统可以针对c语言程序文件进行单 ...
- ng-notadd 0.17.1 发布,基于 Angular 的企业级中后台
中文说明: 支持后台生成截图 修改步进组件的表单字段和按钮样式 修改json-schema-form组件布局样式 修改步进器组件布局样式 技术栈 Typescript Angular Material ...
- ng-notadd 0.10.1,基于 Angular7 和 material2 的中后台解决方案
更新内容 修复 scss 左侧导航栏美化 修复导航栏 2px 间隔问题 技术栈 Typescript Angular Material2 rxjs Graphql 相关链接 项目地址 DEMO ng- ...
- linux下使用gtest框架进行c/c++单元测试
原文地址linux下使用gtest框架进行c/c++单元测试 前言 google test(以下简称gtest)是谷歌的开源C++单元测试框架,用来做c/c++的单元测试比较方便.下面对于它在linu ...
- 玩转Spring JUnit+mockito+powermock单元测试(使用详解)
说明:请耐心看完... Spring中执行单元测试,最麻烦的就是解决Bean的定义以及注入的问题.最开始使用Spring的上下文初始化进行测试,开头是这样的: @RunWith(SpringJUnit ...
- 估算带卷积核二分类0,3的网络的收敛时间和迭代次数
制作一个网络分类minst的0和3求出这网络的迭代次数曲线表达式n(δ),和准确率表达式p-max(δ),用预期准确率去估算n,并推算需要的时间. 将minst的28*28的图片缩小到9*9,网络用一 ...
- python答辩结束语_Beta答辩总结
前言 队名:拖鞋旅游队 项目的链接与宣传 项目总结 原计划 实现功能 预期完成程度 上传照片 完美实现 照片信息标注在地图上 对于有地理信息的照片能够较为精确的定位 足迹地图可视化 能够用颜色区分出到 ...
- 福大软工1816 · 第七次作业 - 需求分析报告之拖鞋旅游队
[组长博客链接] 031602428 苏路明 [计划安排] 阶段 主要任务 时间 任务内容 1 项目选题 09.22 - 10.10 确定选题内容,收集用户需求,明确定位,竞品分析,选题报告 2 需求 ...
- package.json和bower的参数解释
package.json和bower的参数解释 一.package.json解释: package.json是用来声明项目中使用的模块, 这样新的环境部署时,只要在package.json文件所在的目 ...
最新文章
- 打破 Google 破坏性的搜索垄断
- UIMenuController的使用,对UILabel拷贝以及定制菜单
- oracle 控制文件作用是什么,Oracle控制文件(controlfile)作用
- ubuntu18.04的ifconfig输出没有ip地址
- [BZOJ 1046] [HAOI2007] 上升序列 【DP】
- .NET 也有 Husky 了
- 51nod1551-集合交易【hall定理,最大权闭合图,网络流】
- Shader Model 版本与DirectX的关系(OpenGL对应的呢?)
- linux 下查看硬件信息
- 编写可靠shell脚本的八个建议
- sed 去掉最后一行_shell sed命令匹配替换删除最后第一行字符正则表
- 一个程序员父亲的呼吁:不要教你的孩子从小学编程!
- python批量修改文件夹名称,简洁快捷
- Python运行效率低的原因
- 码农与程序员两种不同称呼,有什么本质上的区别?
- 想成为游戏原画师需要哪些系统的学习?
- Android 音乐资源管理与播放
- [Oracle] 一个通过添加本地分区索引提高SQL性能的案例
- 【VMCloud云平台】SCAP(四)租户(二)
- (转!)利用Keras实现图像分类与颜色分类
热门文章
- PHP 绕过禁用函数漏洞的原理与利用分析
- 知名网络安全专家访谈记
- 谷歌8月更新修复50多个漏洞
- BZOJ5329:[SDOI2018]战略游戏(圆方树,虚树)
- 微信小程序框架——微信小程序前端开发工具
- PHP和MySQL Web开发从新手到高手,第9天-总结
- *** cannot be resolved or is not a field
- 注意:不能将文件名叫做email.py,否则会报 ImportError: No module named mime.text
- LINUX警告:检测到时钟错误。您的创建可能是不完整的。-转
- 手把手教你玩转网络编程模型之完成例程(Completion Routine)篇(下)-转