likes
comments
collection
share

go测试包testify就这么用

作者站长头像
站长
· 阅读数 57

它是一个功能比较全的go语言测试框架,同时支持了断言、mock、套件等功能。原生兼容go语言testing包,单看某个功能可能不是最好的,但是整体上来看,它的综合实力非常强。

一、起步

它的使用方式非常简单,基本上和go原生的testing包一样,引入包后直接使用就行,让我们一起看下。

假设我们的main包里有如下代码:

package main

func Add(a, b int) int {
	return a + b
}

下面我们针对此Add函数进行测试,添加测试文件main_test.go

package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestAdd(t *testing.T) {
	// 原生写法
	// got := Add(2, 2)
	// want := 4
	// if got != want {
	// 	t.Errorf("got %q, want %q", got, want)
	// }

	assert.Equal(t, Add(2, 2), 4, "Add(2,2) should be 4")
}

上面使用了testify的断言,断言的用法非常简单,直接调用assert.Equal(t, got, want, "message")即可。 直接在命令行执行go test -v就能看到test相关输出。

非常简单,在使用上基本和go原生的testing包一样。下面我们继续探索。

二、断言

1. assert断言

testify提供了方便的断言功能,这相比原生的got != wantgot == want这种断言方式,更加清晰易读。 断言方式特别多,这里仅介绍常用的。

// 特点:最后一个参数都是断言的描述
assert.Equal(t, Add(2, 2), 4, "Add(2,2) should be 4")
// 不等于
assert.NotEqual(t, 3, 5)
// true or false
assert.True(t, true, "should is true")
assert.False(t, false, "should is false")
// nil
assert.Nil(t, nil)
// contains 包含
// 字符串
assert.Contains(t, "hello world", "world")
// 数组
assert.Contains(t, [3]int{1, 2, 3}, 2)
// map
assert.Contains(t, map[string]int{"a": 1, "b": 2}, "a")
// slice
assert.Contains(t, []string{"a", "b", "c"}, "b")
// error
assert.Error(t, errors.New("a error"), errors.New("a error"))
// empty
assert.Empty(t, []string{})
assert.Empty(t, map[string]int{})
assert.Empty(t, "")
// zero 它检查的是 是否为0值
assert.Zero(t, 0)
assert.Zero(t, 0.0)
assert.Zero(t, false)

2. require断言

除了assert断言外,testify还提供了require断言,它和assert断言类似,assert支持的函数,require也都支持。

它们的区别在于,在一个测试函数中,如果断言失败,是否会继续执行

  • assert断言失败,测试函数继续执行.
  • require断言失败,测试函数直接退出.

PS:是否继续执行,指的是当前测试函数。而不是影响其它函数是否执行。

我们编写如下测试代码,加以说明

// require_test.go
package main

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestRequire(t *testing.T) {
	require.Equal(t, 2, 1)
	fmt.Println("==由于前面require失败,所以这里不会执行=====")
}

func TestAssert(t *testing.T) {
	assert.Equal(t, 2, 1)
	fmt.Println("===虽然前面assert会失败 但是仍会继续执行===")
}

func TestOther(t *testing.T) {
	assert.Equal(t, 1, 1)
	fmt.Println("====其它测试函数照样执行=======")
}

执行结果如下:

dongmingyan@pro ⮀ ~/go_playground/hello ⮀ go test require_test.go
--- FAIL: TestRequire (0.00s)
    require_test.go:12: 
                Error Trace:    /Users/dongmingyan/go_playground/hello/require_test.go:12
                Error:          Not equal: 
                                expected: 2
                                actual  : 1
                Test:           TestRequire
===虽然前面assert会失败 但是仍会继续执行===
--- FAIL: TestAssert (0.00s)
    require_test.go:17: 
                Error Trace:    /Users/dongmingyan/go_playground/hello/require_test.go:17
                Error:          Not equal: 
                                expected: 2
                                actual  : 1
                Test:           TestAssert
====其它测试函数照样执行=======
FAIL
FAIL    command-line-arguments  0.507s
FAIL

符合我们的预期。

3. 断言简写

我们可以看到前面的断言函数在执行时每次都需要传递testing.T参数,这有点麻烦,我们可以优化下。

// 先New一个assert
assert := assert.New(t)
// 然后就可以不带t了
assert.Equal(1, 1)

三、mock

除了断言以外,难能可贵的是,它还支持mock;相比于gomock的繁琐,它相比来说简单了不少。

1. 什么是mock呢?

其实就是这个mock单词的中文含义——模拟,我们在代码中,通常有很多外部依赖,比如数据库、网络请求等,这些外部的依赖我们无法直接控制,所以需要mock。通过mock来模拟这些依赖,让我们的测试只关心我们代码的功能,而不必关心外部的依赖项。

2. 怎么用?

假设我们有如下main.go代码

package main

import "fmt"

type User struct {
	ID   int
	Name string
}

// 一个外部的接口
type Server interface {
	// 有一个GetUser的方法 返回User
	GetUser(id int) User
}

// 打印用户的信息
func GetUserInfo(server Server, id int) string {
	// 依赖于外部的接口Server的GetUser方法
	user := server.GetUser(id)
	return fmt.Sprintf("user id is %d, name is %s", user.ID, user.Name)
}

我们需要测试GetUserInfo这个函数,但是这个函数依赖了一个接口的GetUser方法,我们可以通过mock来实现测试。

main_test.go代码如下:

package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// 第一步:定义一个mock结构体
type ServerMock struct {
	mock.Mock
}

// 第二步:定义一个mock方式(固定的)
func (m *ServerMock) GetUser(id int) User {
	// 这里Called参数要原封不动给到
	args := m.Called(id)
	// 返回值args.Get(0)是一个interface Get(0)代表第一个参数
	return args.Get(0).(User)
}

func TestPrintUserInfo(t *testing.T) {
	// 创建我们事先定义好的mock对象
	server := &ServerMock{}
	// 第三步:设定mock方法的的传参数和返回值
	server.On("GetUser", 1).Return(User{1, "Tom"})
	uinfo := GetUserInfo(server, 1)

	// 断言
	assert.Equal(t, "user id is 1, name is Tom", uinfo)
}

上面的代码已经写了详尽的注释,就不做不过多解释了;我们可以总结的是,它的操作步骤也就三步:

  1. step1:定义一个mock结构体
  2. step2:定义一个mock方式(固定的)
  3. step3:设定mock方法的的传参数和返回值

3. mock代码要求

我们先看一个需要测试的函数:

// 除一个随机数
func divByRand(numerator int) int {
	return numerator / int(rand.Intn(10))
}

我们如何测试这个函数呢?由于rand.Intn(10)是一个随机数,测试时没法通过一个输入值预测输出值,故无法测试。

试想,如果我们能mock出随机数为一个固定数,那么就可以测试。但是上面的代码生成随机数的所有部分rand.Intn(10)都存于函数内部,我们无法mock。

那怎么办呢?可以把随机数的生成部分抽象出来成一个接口,这个接口包含随机数的签名函数即可,优化代码如下:

// mock_example/main.go
package main

import "math/rand"

// 随机数生成器接口
type randGenerator interface {
	randInt(max int) int
}

// 随机数生成器结构体
type standardRand struct{}

// 随机数生成器实现
func (r *standardRand) randInt(max int) int {
	return rand.Intn(max)
}

// 除一个随机数 rg 随机数生成器接口
func divByRand(rg randGenerator, numerator int) int {
	return numerator / int(1+rg.randInt(10))
}

func main() {
	// 创建一个随机数生成器
	rg := &standardRand{}
	// 使用随机数生成器
	divByRand(rg, 100)
}

现在我们可以对divByRand这个函数进行测试了。 测试代码如下:

// mock_example/main_test.go
package main

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

type MockRandGenerator struct {
	mock.Mock
}

func (m *MockRandGenerator) randInt(max int) int {
	args := m.Called(max)
	return args.Int(0)
}

func TestDivByRand(t *testing.T) {
	mockRand := &MockRandGenerator{}
	mockRand.On("randInt", 10).Return(4)

	result := divByRand(mockRand, 10)
	assert.Equal(t, 2, result)
}

由此可见,非常重要的一步是,把依赖项抽象出来,形成接口,然后mock测试。这也是为什么go中可以看到大量的interface的原因之一,它便与测试。

4. 一些技巧

mock除了我们上面看到的on return形式外,还支持一些有意思的技巧。

// 执行一次
xxmock.On("randInt", 10).Return(4).Once()
// 执行二次
xxmock.On("randInt", 10).Return(5).Twice()
// 执行3次
xxmock.On("randInt", 10).Return(6).Times(3)
// 可能被调用,也可能不被调用
xxmock.On("randInt", 10).Return(6).Maybe()
// 任意参数 返回固定值
xxmock.On("MyMethod", mock.Anything).Return("固定返回值")

四、套件

在go内置的testing包中,并没有提供一种将各种测试用例有效组织起来的方式;幸运的是testify为了我们提供了套件功能,它是支持的。

1. 怎么用?

套件怎么使用呢?其实也很简单:

  1. 定义套件(suite)结构体嵌入(suite)
  2. 定义套件结构体方法(测试/辅助)
  3. 定义套件测试函数(引爆点,套件执行入口)

直接看文字不太容易理解,我们还是直接看代码,假设我们要测试购物车相关的功能,代码如下:

// main.go
package main

// 商品条目
type Item struct {
	ID   int
	Name string
}

// 购物车
type ShoppingCart struct {
	Items []Item // 商品列表
	Count int    // 商品数量
}

// 添加购物车
func (s *ShoppingCart) AddItem(item Item) {
	s.Items = append(s.Items, item)
	s.Count++
}

// 移除购物车
func (s *ShoppingCart) RemoveItem(item Item) {
	for i, v := range s.Items {
		if v.ID == item.ID {
			s.Items = append(s.Items[:i], s.Items[i+1:]...)
			s.Count--
			break
		}
	}
}

测试代码:

// main_test.go
package main

import (
	"testing"

	"github.com/stretchr/testify/suite"
)

// =============step1: 定义套件结构体=================
type ShoppingCartSuite struct {
	suite.Suite               // 嵌入套件
	cart        *ShoppingCart // 购物车
}

// ============step2: 定义套件结构体的方法=============
// 初始化套件(套件执行前)
func (s *ShoppingCartSuite) SetupSuite() {
	s.T().Log("=====初始化套件=====")
	s.cart = &ShoppingCart{}
}

// 拆卸套件(套件执行后)
func (s *ShoppingCartSuite) TearDownSuite() {
	s.T().Log("=====拆卸套件=====")
}

// 初始化测试(每个测试执行前 初始化工作)
func (s *ShoppingCartSuite) SetupTest() {
	s.T().Log("=====初始化测试=====")
}

// 拆卸测试(每个测试执行后 清理工作)
func (s *ShoppingCartSuite) TearDownTest() {
	s.T().Log("=====拆卸测试=====")
	s.cart.Count = 0
	s.cart.Items = []Item{}
}

func (s *ShoppingCartSuite) TestAddItem() {
	s.T().Log("=====测试添加商品=====")
	s.cart.AddItem(Item{ID: 1, Name: "apple"})
	s.cart.AddItem(Item{ID: 2, Name: "banana"})
	s.cart.AddItem(Item{ID: 3, Name: "orange"})
	s.Equal(3, s.cart.Count)
}

func (s *ShoppingCartSuite) TestRemoveItem() {
	s.T().Log("=====测试移除商品=====")
	s.cart.AddItem(Item{ID: 1, Name: "apple"})
	s.cart.AddItem(Item{ID: 2, Name: "banana"})
	s.cart.AddItem(Item{ID: 3, Name: "orange"})
	s.cart.RemoveItem(Item{ID: 2, Name: "banana"})
	s.Equal(2, s.cart.Count)
}

// step3: 测试套件引爆点(入口)
func TestShoppingCart(t *testing.T) {
	// 通过suite.Run启动测试
	suite.Run(t, new(ShoppingCartSuite))
}

运行测试go test -v

=== RUN   TestShoppingCart
    shopping_cart_test.go:18: =====初始化套件=====
=== RUN   TestShoppingCart/TestAddItem
    shopping_cart_test.go:29: =====初始化测试=====
    shopping_cart_test.go:40: =====测试添加商品=====
    shopping_cart_test.go:34: =====拆卸测试=====
=== RUN   TestShoppingCart/TestRemoveItem
    shopping_cart_test.go:29: =====初始化测试=====
    shopping_cart_test.go:48: =====测试移除商品=====
    shopping_cart_test.go:34: =====拆卸测试=====
=== NAME  TestShoppingCart
    shopping_cart_test.go:24: =====拆卸套件=====
--- PASS: TestShoppingCart (0.00s)
    --- PASS: TestShoppingCart/TestAddItem (0.00s)
    --- PASS: TestShoppingCart/TestRemoveItem (0.00s)
PASS
ok  	command-line-arguments	0.540s

2. 层级关系

从上面的例子我们可以看出,测试套件有很多层级的设定,可以在套件开始前、结束后、测试开始前、结束后执行某些操作,这在测试时非常用。

比如:我们希望在测试套件开始前,创建一个数据库连接,测试结束后,关闭数据库连接;在某个测试开始前,清理上一个测试用例创建的数据等等。

那么这些层级到底是怎样的?我们梳理下:

SetupSuite # 套件开始前
	SetupTest # 测试开始前(每个测试)
		BeforeTest(suiteName, testName) # 测试前 带测试名(每个测试)
			SetupSubTest() # 子测试开始前
			TearDownSubTest() # 子测试结束后
		AfterTest(suiteName, testName) # 测试后 带测试名(每个测试)
	TearDownTest # 测试结束后
TearDownSuite # 套件结束后

另外suite是支持子测试用法如下:

func (suite *ExampleSuite) TestCase() {
	suite.T().Log("======TestCase=====")

	// 在函数执行后里继续运行就是子测试
	suite.Run("case1-subtest1", func() {
		suite.T().Log("======TestCase.Subtest1=====")
	})
	suite.Run("case1-subtest2", func() {
		suite.T().Log("======TestCase.Subtest2=====")
	})
}
转载自:https://juejin.cn/post/7380200984059117594
评论
请登录