Go单元测试(四)

1,025 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

前几篇文章主要系统的介绍了Go提供的测试相关的SDK的使用,今天要说的是GO三方的一个测试库testify,他可以帮助我们更好,更快,更优雅的编写GO的测试方法。

Testify 主要提供了三个部分

  1. assert断言
  2. mock测试数据
  3. suite测试生命周期钩子

安装testify

go get github.com/stretchr/testify

assert

testify提供了很多类似java的Junit测试类库的assert方法,他可以减少我们没有必要的代码片段,如:

  1. 不用assert
func Add(a, b int) int {
	return a + b
}

func TestAdd(t *testing.T) {
	actual := Add(1, 2)
	if actual != 3 {
		t.Errorf("func Add execute failed, expect: 3, but: %d", actual)
	}
}
  1. 使用assert后,可以直接一行搞定
func TestHelloTestify(t *testing.T) {
	actual := Add(1, 2)
	assert.Equal(t, 3, actual, "func Add execute failed, expect: 3, but: %d", actual)
}

常见的Assert方法

Contains

函数的签名如下:

func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool

被测试的数据可以是一个切片或者map或者字符串

用法示例:

func TestContainsAssert(t *testing.T) {
	data := []int{3, 2, 5, 8, 4, 7, 6, 9}
	val := 1
	assert.Contains(t, data, val, "data must contains %d, but not found", val)
}

ElementsMatch

上面的Contains方法是用于判断一个指定的集合中,是否包含某个元素,而ElementsMatch是用于判断两个集合中的元素是否全部相等,这两个方法可以对比着记忆,这个方法的判断方式并不会严格按照两个集合中元素的顺序判断,只需要两个集合中包含的元素相同即可。

函数签名如下:

func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) 

用法示例:

func TestElementMatchAssert(t *testing.T) {
	data1 := []string{"I", "am", "tony"}
	data2 := []string{"I", "am", "erik"}

	assert.ElementsMatch(t, data1, data2, "elements not equals")
}

Empty

函数签名如下:

func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool 

Empty断言object是空,根据object中存储的实际类型,空的含义不同:

  • 指针:nil
  • 整数:0;
  • 浮点数:0.0;
  • 字符串:空串""
  • 布尔:false;
  • 切片或 channel:长度为 0。

用法示例

func TestEmptyAssert(t *testing.T) {
	data := []string{}

	assert.Empty(t, data, "arr must be empty")
}

NotEmpty

跟Empty恰恰相反

EqualValues

这个断言的是变量值的相等,比Equal宽泛

DirExists

函数的签名如下:

用于判断路径是否存在

func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool 

用法示例:

func TestDirExistAssert(t *testing.T) {
	path := "/user/local/go"
	assert.DirExists(t, path, "parh: %s not exist", path)
}

Error相关

Error

函数签名如下:

func Error(t TestingT, err error, msgAndArgs ...interface{}) bool

Error断言err不为nil

ErrorAs

ErrorAs断言err表示的 error 链中至少有一个和target匹配。这个函数是对标准库中errors.As的包装。

func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
ErrorIs

ErrorIs断言err的 error 链中有target。 函数签名如下:

func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool

require

testify还提供了require包,里面的方法和assert方法的区别就是,可以实现failfast机制,即,报错后,立即返回,便于我们定位程序发生的错误

mock

在日常开发中,我们有时测试的方法需要依赖一些三方的数据,比如获取数据库中的用户列表信息,但是在本地又无法链接数据库,这时候,我们可以对一些数据进行mock处理,使得我们的测试不被阻塞。

Mock获取用户列表方法

  1. 用户结构体
type Person struct {
	Name string
	Age  int
}
  1. 获取用户列表接口
type PersonService interface {
	GetPersonList() ([]Person, error)
}
  1. 真实实现方法
func (p *Person) GetPersonList() ([]*Person, error) {
	// todo get person from db
}
  1. 打印PersonList方法
func PrintPersonList(ps PersonService) {
	personList, err := ps.GetPersonList()
	if err != nil {
		panic(err)
	}
	fmt.Printf("%v\n", personList)
}
  1. mock实现
type PersonMock struct {
	mock.Mock
}

func (pm *PersonMock) GetPersonList() ([]Person, error) {
	args := pm.Called()
	return args.Get(0).([]Person), args.Error(1)
}
  1. mock数据填充&运行测试用例

var personMock = []Person{
	{
		Name: "tom",
		Age:  12,
	},
	{
		Name: "hetty",
		Age:  9,
	},
	{
		Name: "Erik",
		Age:  6,
	},
}

func (pm *PersonMock) GetPersonList() ([]Person, error) {
	args := pm.Called()
	return args.Get(0).([]Person), args.Error(1)
}

func TestGetPersonMock(t *testing.T) {
	caller := new(PersonMock)
	caller.On("GetPersonList").Return(personMock, nil)

	PrintPersonList(caller)
}

  1. caller是mock的实例,当调用GetPersonList方法时,返回两个值(方法GetPersonList的返回值),一个是测试的personList,另一个是error
  2. 对应的在mock方法GetPersonList中通过调用args.Get(idx)或者调用args.Type(idx)获取对应的值,其中Type可以是Error,Int,Float...等基本数据类型。

suite

testify提供了测试套件的功能(TestSuite),testify测试套件只是一个结构体,内嵌一个匿名的suite.Suite结构。测试套件中可以包含多个测试,它们可以共享状态,还可以定义钩子方法执行初始化和清理操作。

// 所有的测试方法执行前会调用该方法
type SetupAllSuite interface {
  SetupSuite()
}

// 所有的测试方法执行后会调用该方法
type TearDownAllSuite interface {
  TearDownSuite()
}

// 每个测试方法执行前都会调用该方法
type SetupTestSuite interface {
  SetupTest()
}

// 每个测试方法执行后都会调用该方法
type TearDownTestSuite interface {
  TearDownTest()
}

演示示例


import (
	"testing"

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

type HelloSuite struct {
	suite.Suite
}

func Subtraction(a, b int) int {
	return a - b
}

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

func (hs *HelloSuite) TestAdd() {
	actual := Add(1, 2)
	hs.Equal(actual, 3, "Add func execute failed, expect: 3, but %d", actual)
}

func (hs *HelloSuite) TestSubtraction() {
	actual := Subtraction(3, 1)
	hs.Equal(actual, 2, "Subtraction func execute failed, expect: 1,but %d", actual)
}

func (hs *HelloSuite) SetupSuite() {
	hs.T().Log("SetupSuite")
}

func (hs *HelloSuite) TearDownSuite() {
	hs.T().Log("TearDownSuite")
}

func (hs *HelloSuite) SetupTest() {
	hs.T().Log("SetupTest")
}

func (hs *HelloSuite) TearDownTest() {
	hs.T().Log("TearDownTest")
}

func TestStart(t *testing.T) {
	suite.Run(t, new(HelloSuite))
}

控制台输出

=== RUN   TestStart
    1_test.go:32: SetupSuite
=== RUN   TestStart/TestAdd
    1_test.go:40: SetupTest
    1_test.go:44: TearDownTest
=== RUN   TestStart/TestSubtraction
    1_test.go:40: SetupTest
    1_test.go:44: TearDownTest
=== CONT  TestStart
    1_test.go:36: TearDownSuite
--- PASS: TestStart (0.00s)
    --- PASS: TestStart/TestAdd (0.00s)
    --- PASS: TestStart/TestSubtraction (0.00s)
PASS