简介
testify的功能包括:
- Easy assertions
- Mocking
- Testing suite interfaces and functions
assert
该assert软件包提供了一些有用的方法,使您可以在Go中编写更好的测试代码。
- 打印友好,易于阅读的故障描述
- 允许可读性非常高的代码
- (可选)用消息注释每个断言
实战:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// assert equality
assert.Equal(t, 123, 123, "they should be equal")
// assert inequality
assert.NotEqual(t, 123, 456, "they should not be equal")
// assert for nil (good for errors)
assert.Nil(t, object)
// assert for not nil (good when you expect something)
if assert.NotNil(t, object) {
// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal(t, "Something", object.Value)
}
}
|
- 每个断言函数都将testing.T对象作为第一个参数。这就是它通过正常
go test
功能将错误写出的方式。
- 每个断言函数都返回一个bool,指示断言是否成功,这对于在某些条件下要继续进行进一步断言很有用。
如果您断言多次,请使用以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
assert := assert.New(t)
// assert equality
assert.Equal(123, 123, "they should be equal")
// assert inequality
assert.NotEqual(123, 456, "they should not be equal")
// assert for nil (good for errors)
assert.Nil(object)
// assert for not nil (good when you expect something)
if assert.NotNil(object) {
// now we know that object isn't nil, we are safe to make
// further assertions without causing any errors
assert.Equal("Something", object.Value)
}
}
|
require
require
包提供与assert
包相同的全局函数,他们的唯一差别就是require的函数会直接导致case结束,而assert虽然也标记为case失败,但case不会退出,而是继续往下执行。
有关详细信息,请参见t.FailNow。
FailNow将函数标记为失败,并通过调用runtime.Goexit(然后运行当前goroutine中的所有延迟调用)来停止执行。 执行将在下一个测试或基准测试中继续。 必须从运行测试或基准功能的goroutine中调用FailNow,而不是从在测试过程中创建的其他goroutine中调用FailNow。 调用FailNow不会停止其他goroutine。
看一个例子:
使用assert:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCase1(t *testing.T) {
name := "Bob"
age := 10
assert.Equal(t, "bob", name)
assert.Equal(t, 20, age)
}
|
执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$ go test
--- FAIL: TestCase1 (0.00s)
assertions.go:254:
Error Trace: main_test.go:13
Error: Not equal:
expected: "bob"
actual : "Bob"
Test: TestCase1
assertions.go:254:
Error Trace: main_test.go:14
Error: Not equal:
expected: 20
actual : 10
Test: TestCase1
FAIL
exit status 1
FAIL testUT 0.009s
|
在这个例子中我们使用的是assert,可以看到两个assert.Equal()指令都被执行了。
使用require:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCase1(t *testing.T) {
name := "Bob"
age := 10
require.Equal(t, "bob", name)
require.Equal(t, 20, age)
}
|
执行:
1
2
3
4
5
6
7
8
9
10
11
|
$ go test
--- FAIL: TestCase1 (0.00s)
assertions.go:254:
Error Trace: main_test.go:12
Error: Not equal:
expected: "bob"
actual : "Bob"
Test: TestCase1
FAIL
exit status 1
FAIL testUT 0.007s
|
而在这个例子中我们使用的是require,可以看到只有第一个require.Equal()指令被执行了,第二个require.Equal()没有被执行。
mock
testify
包另外一个优秀的功能就是它的模拟功能。有效的模拟允许我们在代码里创建一个替代的对象,用来模拟对象的某些行为,这样我们在运行测试用例时就不用每次都期望它能够触发。
例如,一个是消息服务或电子邮件服务,无论何时被调用,都会向客户端发送电子邮件。如果我们正在积极地开发我们的代码库,可能每天会运行数百次测试,但我们不希望每天向客户发送数百封电子邮件或消息,因为那样他们可能会不高兴。
那么,我们要如何使用 testify
包来模拟呢?
示例一
让我们来看一下如何将 mocks
应用到一个相当简单的例子中。在这个例子中,我们有一个系统会尝试向客户收取产品或服务的费用。当 ChargeCustomer()
被调用时,它将随后调用 Message Service
,向客户发送 SMS
文本消息来通知他们已经被收取的金额。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package main
import (
"fmt"
)
// MessageService 通知客户被收取的费用
type MessageService interface {
SendChargeNotification(int) error
}
// SMSService 是 MessageService 的实现
type SMSService struct{}
// MyService 使用 MessageService 来通知客户
type MyService struct {
messageService MessageService
}
// SendChargeNotification 通过 SMS 来告知客户他们被收取费用
// 这就是我们将要模拟的方法
func (sms SMSService) SendChargeNotification(value int) error {
fmt.Println("Sending Production Charge Notification")
return nil
}
// ChargeCustomer 向客户收取费用
// 在真实系统中,我们会模拟这个
// 但是在这里,我想在每次运行测试时都赚点钱
func (a MyService) ChargeCustomer(value int) error {
a.messageService.SendChargeNotification(value)
fmt.Printf("Charging Customer For the value of %d\n", value)
return nil
}
// 一个 "Production" 例子
func main() {
fmt.Println("Hello World")
smsService := SMSService{}
myService := MyService{smsService}
myService.ChargeCustomer(100)
}
|
那么,我们如何进行测试以确保我们不会让客户疯掉?好吧,我们通过创建一个新的 struct
称之为 smsServiceMock
,用来模拟我们的 SMSService
,并且将 mock.Mock
添加到它的字段列表中。
然后我们将改写 SendChargeNotification
方法,这样它就不会向我们的客户发送通知并返回 nil 错误。
最后,我们创建 TestChargeCustomer
测试函数,接着实例化一个新的类型实例 smsServiceMock
并指定 SendChargeNotification
在被调用时应该做什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/mock"
)
// smsServiceMock
type smsServiceMock struct {
mock.Mock
}
// 我们模拟的 smsService 方法
func (m *smsServiceMock) SendChargeNotification(value int) bool {
fmt.Println("Mocked charge notification function")
fmt.Printf("Value passed in: %d\n", value)
// 这将记录方法被调用以及被调用时传进来的参数值
args := m.Called(value)
// 它将返回任何我需要返回的
// 这种情况下模拟一个 SMS Service Notification 被发送出去
return args.Bool(0)
}
// 我们将实现 MessageService 接口
// 这就意味着我们不得不改写在接口中定义的所有方法
func (m *smsServiceMock) DummyFunc() {
fmt.Println("Dummy")
}
// TestChargeCustomer 是个奇迹发生的地方
// 在这里我们将创建 SMSService mock
func TestChargeCustomer(t *testing.T) {
smsService := new(smsServiceMock)
// 然后我们将定义当 100 传递给 SendChargeNotification 时,需要返回什么
// 在这里,我们希望它在成功发送通知后返回 true
smsService.On("SendChargeNotification", 100).Return(true)
// 接下来,我们要定义要测试的服务
myService := MyService{smsService}
// 然后调用方法
myService.ChargeCustomer(100)
// 最后,我们验证 myService.ChargeCustomer 调用了我们模拟的 SendChargeNotification 方法
smsService.AssertExpectations(t)
}
|
所以,当我们运行 go test ./... -v
时,我们应该看到以下输出:
1
2
3
4
5
6
7
8
9
|
go test ./... -v
=== RUN TestChargeCustomer
Mocked charge notification function
Value passed in: 100
Charging Customer For the value of 100
--- PASS: TestChargeCustomer (0.00s)
main_test.go:33: PASS: SendChargeNotification(int)
PASS
ok _/Users/elliot/Documents/Projects/tutorials/golang/go-testify-tutorial 0.012s
|
正如你所看到的,我们的模拟方法被调用了而不是 “ production ” 方法,这证明我们的 myService.ChargeCustomer()
方法按照我们所期望的方式在运行!
示例二
很多时候,测试环境不具备 routine 执行的必要条件。比如查询 consul 里的 KV,即使准备了测试consul,也要先往里面塞测试数据,十分麻烦。又比如查询 AWS S3 的文件列表,每个开发人员一个测试 bucket 太混乱,大家用同一个测试 bucket 更混乱。必须找个方式伪造 consul client 和 AWS S3 client。通过伪造 consul client 查询 KV 的方法,免去连接 consul, 直接返回预设的结果。
首先考虑一下怎样伪造 client。假设 client
被定义为 var client *SomeClient
。当 SomeClient
是 type SomeClient struct{...}
时,我们永远没法在 test 环境修改 client
的行为。当是 type SomeClient interface{...}
时,我们可以在测试代码中实现一个符合 SomeClient interface
的 struct
,用这个 struct
的实例替换原来的 client
。
假设一个 IP 限流程序从 consul 获取阈值并更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
type SettingGetter interface {
Get(key string) ([]byte, error)
}
type ConsulKV struct {
kv *consul.KV
}
func (ck *ConsulKV) Get(key string) (value []byte, err error) {
pair, _, err := ck.kv.Get(key, nil)
if err != nil {
return nil, err
}
return pair.Value, nil
}
type IPLimit struct {
Threshold int64
SettingGetter SettingGetter
}
func (il *IPLimit) UpdateThreshold() error {
value, err := il.SettingGetter.Get(KeyIPRateThreshold)
if err != nil {
return err
}
threshold, err := strconv.Atoi(string(value))
if err != nil {
return err
}
il.Threshold = int64(threshold)
return nil
}
|
因为 consul.KV
是个 struct
,没法方便替换,而我们只用到它的 Get 功能,所以简单定义一个 SettingGetter
,ConsulKV
实现了这个接口,IPLimit
通过 SettingGetter
获得值,转换并更新。
在测试的时候,我们不能使用 ConsulKV
,需要伪造一个 SettingGetter
,像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type MockSettingGetter struct {}
func (m *MockSettingGetter) Get(key string) ([]byte, error) {
if key == "threshold" {
return []byte("100"), nil
}
if key == "nothing" {
return nil, fmt.Errorf("notfound")
}
...
}
ipLimit := &IPLimit{SettingGetter: &MockSettingGetter{}}
// ... test with ipLimit
|
这样的确可以隔离 test 对 consul 的访问,但不方便 Table Driven。可以使用 testfiy/mock 改造一下,变成下面这样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import "github.com/stretchr/testify/mock"
type MockSettingGetter struct {
mock.Mock
}
func (m *MockSettingGetter) Get(key string) (value []byte, err error) {
args := m.Called(key)
return args.Get(0).([]byte), args.Error(1)
}
func TestUpdateThreshold(t *testing.T) {
tests := []struct {
v string
err error
rs int64
hasErr bool
}{
{v: "1000", err: nil, rs: 1000, hasErr: false},
{v: "a", err: nil, rs: 0, hasErr: true},
{v: "", err: fmt.Errorf("consul is down"), rs: 0, hasErr: true},
}
for idx, test := range tests {
mockSettingGetter := new(MockSettingGetter)
mockSettingGetter.On("Get", mock.Anything).Return([]byte(test.v), test.err)
limiter := &IPLimit{SettingGetter: mockSettingGetter}
err := limiter.UpdateThreshold()
if test.hasErr {
assert.Error(t, err, "row %d", idx)
} else {
assert.NoError(t, err, "row %d", idx)
}
assert.Equal(t, test.rs, limiter.Threshold, "thredshold should equal, row %d", idx)
}
}
|
再说到上面提到的 AWS S3,AWS 的 Go SDK 已经给我们定义好了 API 的 interface,每个服务下都有个 xxxiface 目录,比如 S3 的是 github.com/aws/aws-sdk-go/service/s3/s3iface
,如果查看它的源码,会发现它的 API interface 列了一大堆方法,将这几十个方法都伪造一次而实际中只用到一两个显得很蠢。要想没那么蠢,一个方法是将 S3 的 API 像上面那样再封装一下,另一个方法可以像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import (
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3iface"
)
type MockS3API struct {
s3iface.S3API
mock.Mock
}
func (m *MockS3API) ListObjects(input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
args := m.Called(input)
return args.Get(0).(*s3.ListObjectsOutput), args.Error(1)
}
|
struct 里内嵌一个匿名 interface,免去定义无关方法的苦恼。
用 Mockery 生成模仿对象
在上面的例子中,我们自己模拟了所有的方法,但在实际的例子中,这可能意味着有海量的方法和函数需要来模拟。
值得庆幸的是,这里有 vektra/mockery
包来当我们的好帮手。
mockry 的二进制文件可以找到任何你在 Go 中定义的 interfaces 的名字,然后会自动输出生成模仿对象到 mocks/InterfaceName.go
。当你想节省大量时间时,这非常的方便,我强烈建议你使用这个工具!
suite
该suite软件包提供了一些您可能习惯于使用更多面向对象语言的功能。使用它,您可以将suite构建为结构,在结构上构建 setup/teardown 方法和 testing 方法,并按正常方式使用“go test”来运行它们。
suite示例如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// Basic imports
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}
// All methods that begin with "Test" are run as tests within a
// suite.
func (suite *ExampleTestSuite) TestExample() {
assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
}
// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
|
有关更完整的示例,请使用套件包提供的所有功能,请查看我们的示例测试套件
有关编写套件的更多信息,请查看软件包的API文档suite。
Suite 对象具有assert方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// Basic imports
import (
"testing"
"github.com/stretchr/testify/suite"
)
// Define the suite, and absorb the built-in basic suite
// functionality from testify - including assertion methods.
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}
// All methods that begin with "Test" are run as tests within a
// suite.
func (suite *ExampleTestSuite) TestExample() {
suite.Equal(suite.VariableThatShouldStartAtFive, 5)
}
// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
|
参考:
https://www.jianshu.com/p/ad46bbbf877c
https://studygolang.com/articles/16799
https://zhuanlan.zhihu.com/p/63073179