简介

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。当 SomeClienttype SomeClient struct{...} 时,我们永远没法在 test 环境修改 client 的行为。当是 type SomeClient interface{...} 时,我们可以在测试代码中实现一个符合 SomeClient interfacestruct,用这个 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 功能,所以简单定义一个 SettingGetterConsulKV 实现了这个接口,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