之前写go,一直使用的就是原生的test,测试时则是直接虚拟环境启动。最近遇到了,无法使用虚拟环境,参数又非常多的情况,原生的test的支持就不够了。因此需要进行一些学习,记录以下。其实主要就是一些第三方包的使用。
依赖注入 dig
dig
是uber开源的基于go语言的依赖注入框架,帮助开发者管理系统中对象的创建和
维护,遵循设计模式的IOC原则。一般流程如下:
- 创建一个容器:
dig.New
- 注册构造函数:为想要让
dig
容器管理的实例创建构造函数,构造函数可以有多个参数和多个返回值,这些参数是这些返回值的依赖,这些返回值都会被容器管理 - 使用这些实例:编写一个函数,将需要使用的实例作为参数,然后调用Invoke执行我们编写的函数。框架在容器中找到函数的参数类型对应的实例,并调用函数
当参数过多时,代码会变得非常难读。dig
支持将全部的参数打包,只需要将dig.in
内嵌进该类型中。
1
2
3
4
5
6
7
8
9
10
11
12
type Params {
dig.In
Arg1 *Arg1
Arg2 *Arg2
Arg3 *Arg3
Arg4 *Arg4
}
container.Provide(func (params Params) *Object {
// ...
})
内嵌了dig.In
之后,dig
会将该类型中的其它字段看成Object
的依赖,创建Object
类型的对象时,会先将依赖的Arg1/Arg2/Arg3/Arg4
创建好。
而同样的,如果构造函数返回多个值,这些不同类型的值都会存储到dig
容器中。参数过多会影响代码的可读性和可维护性,返回值过多同样也是如此。为此,dig
提供了返回值对象,返回一个包含多个类型对象的对象。返回的类型,必须内嵌dig.Out
:
1
2
3
4
5
6
7
8
9
10
11
type Results struct {
dig.Out
Result1 *Result1
Result2 *Result2
Result3 *Result3
Result4 *Result4
}
dig.Provide(func () (Results, error) {
// ...
})
参考
ps:下面这个链接讲解的非常详细,从依赖注入的需求场景的小例子一点点讲的,连名词都解释。强推,读完感觉自己懂完了。还有这个人推荐了wire
,谷歌自己开发的依赖注入,暂时用不到,之后再看
Testify
之前主要用go的原生test,功能还是比较局限的,比如不支持断言,因此使用第三方包就可以对开发产生帮助。
1.使用assert进行断言,典型的写法如下
1
2
assert.True(t, IsPalindrome("detartrated"))
assert.False(t, IsPalindrome("palindrome"))
因为testify底层基于go test,因此assert需要传入testing.T,可以用如下的简化
1
2
assert := assert.New(t)
assert.XXXX(xxx)
将 Testify 与表驱动测试相结合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCalculate(t *testing.T) {
assert := assert.New(t) //这样简化后续传参
var tests = []struct {
input int
expected int
}{
{2, 4},
{-1, 1},
{0, 2},
{-5, -3},
{99999, 100001},
}
for _, test := range tests {
assert.Equal(Calculate(test.input), test.expected)
}
}
参考
Improving Your Go Tests and Mocks With Testify
Top Go Modules: Writing Unit Tests with Testify
go的stub:monkey patch
stub和mock的区别在于,前者关注测试对象行为,后者关注测试对象状态。
具体使用上,对函数打桩见如下的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestIsConfigFileContain_MonkeyPatch(t *testing.T) {
ast := assert.New(t)
// 对os.Open进行打桩,固定返回(&os.File{}, nil)
monkey.Patch(os.Open, func(name string) (*os.File, error) {
return &os.File{}, nil
})
// 对ioutil.ReadAll进行打桩,固定返回([]byte("test for monkey patch"), nil)
monkey.Path(ioutil.ReadAll, func(r io.Reader) ([]byte, error) {
return []byte("test for monkey patch"), nil
})
// 测试用例结束时,解除打桩,避免影响其他用例
defer monkey.UnpatchAll()
assert.True(isConfigFileContain("test"))
}
monkey patch对函数进行打桩的API很简单,形式如下monkey.Patch(target function, <replacement function>)
,需要注意的是,在用例结束之后,记得调用monkey.UnpatchAll
来解除打桩,避免影响其他用例。
对于使用monkey patch为方法进行打桩的用法为monkey.PatchInstanceMethod(<type>, <name>, <replacement>)
,其中type
通过reflect.TypeOf
获得,值得注意的是,type
必须跟方法定义的接收者类型一致,如上述代码中,Client.Get
方法的接收者是指针类型,因此type
必须声明为*Client
类型。
日志库:zap
日志库显然也是测试中十分重要的一类功能,
go的原生logger提供了基本功能,优劣总结如下:
- 优势
它最大的优点是使用非常简单。我们可以设置任何io.Writer
作为日志记录输出并向其发送要写入的日志。
劣势
仅限基本的日志级别
- 只有一个
Print
选项。不支持INFO
/DEBUG
等多个级别。
- 只有一个
对于错误日志,它有
Fatal
和Panic
Fatal日志通过调用
os.Exit(1)
来结束程序Panic日志在写入日志消息之后抛出一个panic
缺少一个ERROR日志级别,这个级别可以在不抛出panic或退出程序的情况下记录错误
缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。
不提供日志切割的能力。
因此需要引入zap。
Zap提供了两种类型的日志记录器—Sugared Logger
和Logger
。
在性能很好但不是很关键的上下文中,使用SugaredLogger
。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。