Review 《github.com/stretchr/testify》


testify 是一组工具包,它里面的很多工具可以让你更舒适地写 Go 测试代码。

它包含以下特征:

知识索引:

assert

assert 包提供了一些有用的方法,可以让你写出更友好的测试代码。它具有以下特性:

  • 打印友好,很容易去阅读错误信息
  • 可以开发出易阅读的代码
  • 可以在每个assert后添加一个可选的注解信息

让我们通过一个实际例子来了解它:

package testify_exam

import (
	"testing"

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

func TestSomething(t *testing.T) {
	// 测试相等
	assert.Equal(t, 123, 123, "they should be equal")

	// 测试不相等
	assert.NotEqual(t, 123, 456, "they should not be equal")

	var obj chan struct{}
	// 测试 nil,常用于错误值的判断
	assert.Nil(t, obj)

	obj2 := struct {
		Value string
	}{
		Value: "Something",
	}
	// 测试不为 nil,常用于你希望返回的结果值拥有某些东西的时候
	if assert.NotNil(t, obj2) {
		// 现在我们知道 obj2 的值不是 nil了,我们可以在这个基础上**安全地**访问它的字段。
		assert.Equal(t, "Something", obj2.Value)
	}
}
  • 每个assert函数都使用 testing.T 来作为它的第一个参数,这就是它通过正常的 go 测试功能将错误输出写出来的方式。
  • 每个assert函数返回了一个布尔值,表示这个断言成功与否,如果你想在某个断言结果的基础上做出更多的断言,可以用到它的返回值。

如果你使用断言很多次,可以使用如下的代码:

package testify_exam

import (
	"testing"

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

func TestEasyAssert(t *testing.T) {
	assert := assert.New(t)

	assert.Equal(123, 123, "they should be equal")

	assert.NotEqual(123, 456, "they should not be equal")

	var obj chan struct{}
	assert.Nil(obj)

	obj2 := struct {
		Value string
	}{
		Value: "Something",
	}
	if assert.NotNil(obj2) {
		assert.Equal("Something", obj2.Value)
	}
}

require

require包中拥有和assert包相同的全局函数,它和assert的不同在于,它不会再返回一个布尔值表示测试运行成功与否,而是直接结束当前测试

可以参考 t.FailNow 函数了解更多的信息

下面是一个require包和assert包的比较示例:

package testify_exam

import (
	"sync/atomic"
	"testing"

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

// `require.True` 失败后会将测试 TestEx1 停掉,不会再执行后面的 require.Equal 断言
// `assert.True` 失败后并不会将 TestEx2 停掉,在后面的断言中,Add 依然会被执行,
// 所以 Add 会在 TestEx2 中执行一次, count 的最终结果是1

var count int32 = 0

func Add(a, b int) int {
	atomic.AddInt32(&count, 1)
	return a + b
}

func TestEx1(t *testing.T) {
	require.True(t, false)
	require.Equal(t, Add(1, 1), 2)
}

func TestEx2(t *testing.T) {
	assert.True(t, false)
	assert.Equal(t, Add(1, 1), 2)
	assert.Equal(t, count, int32(1))
}

// 下面是程序的运行结果
//>>> go test -v testify_exam/require_test.go                                                                                                                      23:30:19 (06-02)
//=== RUN   TestEx1
//--- FAIL: TestEx1 (0.00s)
//Error Trace:    require_test.go:19
//Error:          Should be true
//=== RUN   TestEx2
//--- FAIL: TestEx2 (0.00s)
//Error Trace:    require_test.go:24
//Error:          Should be true
//FAIL
//FAIL    command-line-arguments  0.018s

mock

mock提供了一种机制,可以在编写测试代码的时候轻松编写可用于替代实际对象的模拟对象。

下面是一段示例代码,它测试了一段依赖于外部对象testObj的代码,并且能够设置期望(testify)且断言他们确实发生了。

package yours

import (
  "testing"
  "github.com/stretchr/testify/mock"
)

/*
  Test objects
*/

// MyMockedObject is a mocked object that implements an interface
// that describes an object that the code I am testing relies on.
type MyMockedObject struct{
  mock.Mock
}

// DoSomething is a method on MyMockedObject that implements some interface
// and just records the activity, and returns what the Mock object tells it to.
//
// In the real object, this method would do something useful, but since this
// is a mocked object - we're just going to stub it out.
//
// NOTE: This method is not being tested here, code that uses this object is.
func (m *MyMockedObject) DoSomething(number int) (bool, error) {

  args := m.Called(number)
  return args.Bool(0), args.Error(1)

}

/*
  Actual test functions
*/

// TestSomething is an example of how to use our test object to
// make assertions about some target code we are testing.
func TestSomething(t *testing.T) {

  // create an instance of our test object
  testObj := new(MyMockedObject)

  // set up expectations
  testObj.On("DoSomething", 123).Return(true, nil)

  // call the code we are testing
  targetFuncThatDoesSomethingWithObj(testObj)

  // assert that the expectations were met
  testObj.AssertExpectations(t)


}

// TestSomethingWithPlaceholder is a second example of how to use our test object to
// make assertions about some target code we are testing.
// This time using a placeholder. Placeholders might be used when the
// data being passed in is normally dynamically generated and cannot be
// predicted beforehand (eg. containing hashes that are time sensitive)
func TestSomethingWithPlaceholder(t *testing.T) {

  // create an instance of our test object
  testObj := new(MyMockedObject)

  // set up expectations with a placeholder in the argument list
  testObj.On("DoSomething", mock.Anything).Return(true, nil)

  // call the code we are testing
  targetFuncThatDoesSomethingWithObj(testObj)

  // assert that the expectations were met
  testObj.AssertExpectations(t)


}

// TestSomethingElse2 is a third example that shows how you can use
// the Unset method to cleanup handlers and then add new ones.
func TestSomethingElse2(t *testing.T) {

  // create an instance of our test object
  testObj := new(MyMockedObject)

  // set up expectations with a placeholder in the argument list
  mockCall := testObj.On("DoSomething", mock.Anything).Return(true, nil)

  // call the code we are testing
  targetFuncThatDoesSomethingWithObj(testObj)

  // assert that the expectations were met
  testObj.AssertExpectations(t)

  // remove the handler now so we can add another one that takes precedence
  mockCall.Unset()

  // return false now instead of true
  testObj.On("DoSomething", mock.Anything).Return(false, nil)

  testObj.AssertExpectations(t)
}
2019年06月02日 / 22:41