持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
前几篇文章主要系统的介绍了Go提供的测试相关的SDK的使用,今天要说的是GO三方的一个测试库testify
,他可以帮助我们更好,更快,更优雅的编写GO的测试方法。
Testify 主要提供了三个部分
- assert断言
- mock测试数据
- suite测试生命周期钩子
安装testify
go get github.com/stretchr/testify
assert
testify提供了很多类似java的Junit测试类库的assert方法,他可以减少我们没有必要的代码片段,如:
- 不用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)
}
}
- 使用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获取用户列表方法
- 用户结构体
type Person struct {
Name string
Age int
}
- 获取用户列表接口
type PersonService interface {
GetPersonList() ([]Person, error)
}
- 真实实现方法
func (p *Person) GetPersonList() ([]*Person, error) {
// todo get person from db
}
- 打印PersonList方法
func PrintPersonList(ps PersonService) {
personList, err := ps.GetPersonList()
if err != nil {
panic(err)
}
fmt.Printf("%v\n", personList)
}
- mock实现
type PersonMock struct {
mock.Mock
}
func (pm *PersonMock) GetPersonList() ([]Person, error) {
args := pm.Called()
return args.Get(0).([]Person), args.Error(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)
}
- caller是mock的实例,当调用
GetPersonList
方法时,返回两个值(方法GetPersonList
的返回值),一个是测试的personList,另一个是error
- 对应的在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