在 testify 测试框架中,assert 包应该是在日常测试过程中使用最多的。它提供了丰富的断言函数,能帮助我们高效地进行单元测试和结果验证。
assert 包概览
testify 提供的 assert 函数很多,每种函数签名都有两个版本:一个函数名带f,另一个不带f。主要区别在于,带 f 的方法需要指定至少两个参数,一个格式化字符串 format 和若干个参数 args。通过这种方式,你可以在测试运行失败之后,给出自定义的、更清晰的错误信息。你可以根据自身情况来决定使用哪一种形式的断言。
以 Equal 和 Equalf 为例,它们的函数签名如下:
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if err := validateEqualArgs(expected, actual); err != nil {
return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)",
expected, actual, err), msgAndArgs...)
}
if !ObjectsAreEqual(expected, actual) {
diff := diff(expected, actual)
expected, actual = formatUnequalValues(expected, actual)
return Fail(t, fmt.Sprintf("Not equal: \n"+
"expected: %s\n"+
"actual : %s%s", expected, actual, diff), msgAndArgs...)
}
return true
}
func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Equal(t, expected, actual, append([]interface{}{msg}, args...)...)
}
一个简单的使用示例如下所示:
func TestAddUInt64(t *testing.T) {
var expectValue uint64 = 30
actualValue := Add[uint64](10, 21)
assert.Equalf(t, expectValue, actualValue, "expect value %d actual value %d", expectValue, actualValue)
}
常用断言函数解析
下面我们来详细看看 assert 包中一些最常用的函数。
Equal & NotEqual
Equal 主要用于判断两个值是否严格相等,是使用频率最高的断言之一。testify 支持两种调用方式:一种是传统的 assert.Equal(t, expected, actual, msg),另一种是先创建 assert 对象 assert := assert.New(t),再调用 assert.Equal(expected, actual, msg)。示例如下:
import (
"testing"
"github.com/stretchr/testify/assert"
)
// 第一种断言方式
func TestAddInt(t *testing.T) {
expectValue := 30
actualValue := Add(10, 20)
assert.Equal(t, expectValue, actualValue, "expect value and actual value should be equal")
}
// 第二种断言方式
func TestAddFloat32(t *testing.T) {
assert := assert.New(t)
var expectValue float32 = 30.03
actualValue := Add[float32](10.01, 20.02)
// 注意这里的区别
assert.Equal(expectValue, actualValue, "expect value and actual value should be equal")
}
func TestAddUInt32(t *testing.T) {
var expectValue uint32 = 30
actualValue := Add[uint32](10, 20)
assert.NotEqualf(t, expectValue, actualValue, "expect value %d should not equal actual value %d", expectValue, actualValue)
}
Contains & NotContains
Contains 用于判断是否存在包含关系。它的函数签名是 func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool。其中,参数 s 可以是字符串、数组/切片或 map,相应地 contains 应为子串、数组/切片元素或 map 的键。NotContains 则相反,用于判断是否不存在包含关系。
func TestContainsStr(t *testing.T) {
a, b := "Surpass", "Evan"
assert.Containsf(t, a, b, "%s should contains %s", a, b)
}
func TestContaisArray(t *testing.T) {
a, b := [...]int{1, 2, 3, 4, 5}, [...]int{1, 2}
assert.Containsf(t, a, b, "%v should contains %v", a, b)
}
func TestNotContaisArray(t *testing.T) {
a, b := [...]int{1, 2, 3, 4, 5}, 1
assert.NotContainsf(t, a, b, "%v should not contains %v", a, b)
}
DirExists & NoDirExists
这两个函数用于判断某个目录是否存在。DirExists 断言目录存在,NoDirExists 断言目录不存在。
func TestDirExist(t *testing.T) {
// 目录不存在
path := "F:\\tmp"
assert.DirExistsf(t, path, "%s should exist", path)
}
func TestDirNotExist(t *testing.T) {
// 目录不存在
path := "F:\\tmp"
assert.NoDirExists(t, path, "%s should not exist", path)
}
ElementsMatch
ElementsMatch 用于判断两个数组或切片是否包含相同的元素集合,但忽略元素出现的顺序。需要注意的是,如果存在重复的元素,则重复的次数也必须相等。
func TestElementsMatch(t *testing.T) {
a := [...]int{1, 2, 3, 4, 2}
b := [...]int{1, 2, 2, 3, 4}
c := [...]int{1, 2, 2, 2, 3}
assert.ElementsMatchf(t, a, b, "a:%v should match b:%v", a, b)
assert.ElementsMatchf(t, a, c, "a:%v should match c:%v", a, c) // 此断言会失败
}
Empty & Nil
Empty 用于判断给定的值是否为其类型的“零值”或逻辑上的“空”(如空字符串、空切片、nil等)。而 Nil 则专用于断言一个对象的值是否为 nil。
func TestIsEmpty(t *testing.T) {
a := [...]int{1}
assert.Emptyf(t, a, "a value %v should be empty", a)
b := 0
assert.Emptyf(t, b, "b value %v should be empty", b)
c := struct{}{}
assert.Emptyf(t, c, "b value %v should be empty", c)
}
func TestObjIsNil(t *testing.T) {
f := func(a, b int) (int, error) {
if b == 0 {
return math.MaxInt, errors.New("b is equal 0")
}
return a / b, nil
}
t.Run("返回错误为nil-使用Nil来断言", func(t *testing.T) {
_, err := f(10, 2)
assert.Nil(t, err)
})
t.Run("返回错误为nil-使用Empty来断言", func(t *testing.T) {
_, err := f(10, 2)
assert.Empty(t, err)
})
}
EqualValues
EqualValues 也用于判断两个值是否“相等”,但它比 Equal 更宽松。如果两个值的类型不同,EqualValues 会尝试将它们转换为更高精度的公共类型后再进行比较(例如 int32 和 int64)。
func TestEqualValues(t *testing.T) {
var a int32 = 30
var b int64 = 30
var c float64 = 30.0
assert.EqualValuesf(t, a, b, "a type %T value %d should equal %T value %d", a, a, b, b)
assert.EqualValuesf(t, a, c, "a type %T value %d should equal %T value %d", a, a, c, c)
}
Error & NoError & EqualError
在Go中处理错误是常态,assert 也提供了相应的断言。Error 断言返回的 error 不为 nil;NoError 断言返回的 error 为 nil;EqualError 则断言返回的 error 的字符串描述等于指定的字符串。
func TestError(t *testing.T) {
nilError := func() error {
return nil
}
errSample := func() error {
return errors.New("some error happend")
}
assert.Error(t, errSample(), "error is not nil")
assert.NoError(t, nilError(), "some error happend")
}
func TestEqualError(t *testing.T) {
errFunc := func() error {
return errors.New("error sample test")
}
assert.EqualError(t, errFunc(), "error sample test")
}
ErrorAs & ErrorIs
这两个函数用于处理更复杂的错误链判断。
ErrorAs:判断返回的错误是否可以被转换为指定的目标错误类型。常用于检查错误链中是否包裹了特定类型的错误。
ErrorIs:判断返回的错误是否等于(使用 errors.Is)指定的错误值。常用于检查错误链中是否包含某个特定的 sentinel error。
使用标准库错误的示例:
func TestErrorAs(t *testing.T) {
var pathError *os.PathError
_, err := os.Open("/tmp/surpass.txt")
assert.ErrorAsf(t, err, &pathError, "expect error %T,acutal error %T", pathError, err)
}
func TestErrorIs(t *testing.T) {
_, err := os.Open("/tmp/surpass.txt")
assert.ErrorIs(t, err, os.ErrNotExist, "...")
}
也可以用于自定义错误类型的断言:
// 自定义错误类型
type MyError struct {
Code int
Message string
}
// 实现Error方法
func (me *MyError) Error() string {
return fmt.Sprintf("code %d - %s", me.Code, me.Message)
}
func MockError() error {
innerError := &MyError{
Code: 404,
Message: "Not Found",
}
return fmt.Errorf("Mock error:%w", innerError)
}
func TestErrorAs(t *testing.T) {
var myError *MyError
err := MockError()
assert.ErrorAs(t, err, &myError, "")
}
// 自定义错误类型
var ErrNotFound = errors.New("Not Found")
var ErrPermission = errors.New("Permission Denied")
func TestErrorAs(t *testing.T) {
f := func(id int) error {
if id == 0 {
return ErrNotFound
} else if id < 0 {
return errors.Join(ErrPermission, ErrNotFound)
}
return nil
}
t.Run("返回特定错误", func(t *testing.T) {
assert.ErrorIs(t, f(0), ErrNotFound)
})
t.Run("返回多个错误", func(t *testing.T) {
assert.ErrorIs(t, f(-5), ErrPermission)
})
t.Run("没有错误,使用Empty判断", func(t *testing.T) {
assert.Empty(t, f(10))
})
t.Run("没有错误,使用ErrorIs判断", func(t *testing.T) {
assert.ErrorIs(t, f(10), nil)
})
}
通过掌握这些核心的 assert 断言函数,你可以为你的 Go 项目编写出更健壮、更清晰的单元测试代码,从而有效提升代码质量和开发效率。如果你想了解更多关于测试框架或最佳实践,欢迎在云栈社区与其他开发者交流探讨。