简介

testifytestifyasserttesting
testify
assertmocksuite

准备工作

本文代码使用 Go Modules。

创建目录并初始化:

$ mkdir -p testify && cd testify
$ go mod init github.com/darjun/go-daily-lib/testify
testify
$ go get -u github.com/stretchr/testify

assert

assert判断 + 信息输出的模式
if got != expected {
  t.Errorf("Xxx failed expect:%d got:%d", got, expected)
}

简化为一行断言代码:

assert.Equal(t, got, expected, "they should be equal")
assertassert
func TestEqual(t *testing.T) {
  var a = 100
  var b = 200
  assert.Equal(t, a, b, "")
}
testifytesting_test.goTestXxxgo test
$ go test

--- FAIL: TestEqual (0.00s)
    assert_test.go:12:
                Error Trace:
                Error:          Not equal:
                                expected: 100
                                actual  : 200
                Test:           TestEqual
FAIL
exit status 1
FAIL    github.com/darjun/go-daily-lib/testify/assert   0.107s

我们看到信息更易读。

testifyassertfffformatargs
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})
Equalf()Equal()
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...)...)
}
f

Contains

函数类型:

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

DirExists

函数类型:

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

ElementsMatch

函数类型:

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

Empty

函数类型:

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

EqualError

函数类型:

func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
EqualErrortheError.Error()errString

EqualValues

函数类型:

func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
EqualValuesexpectedactualEqualEqual()trueEqualValues()truereflect.DeapEqual()
func ObjectsAreEqual(expected, actual interface{}) bool {
  if expected == nil || actual == nil {
    return expected == actual
  }
  exp, ok := expected.([]byte)
  if !ok {
    return reflect.DeepEqual(expected, actual)
  }
  act, ok := actual.([]byte)
  if !ok {
    return false
  }
  if exp == nil || act == nil {
    return exp == nil && act == nil
  }
  return bytes.Equal(exp, act)
}
func ObjectsAreEqualValues(expected, actual interface{}) bool {
    // 如果`ObjectsAreEqual`返回 true,直接返回
  if ObjectsAreEqual(expected, actual) {
    return true
  }
  actualType := reflect.TypeOf(actual)
  if actualType == nil {
    return false
  }
  expectedValue := reflect.ValueOf(expected)
  if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
    // 尝试类型转换
    return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
  }
  return false
}
intMyIntEqual()EqualValues()
type MyInt int
func TestEqual(t *testing.T) {
  var a = 100
  var b MyInt = 100
  assert.Equal(t, a, b, "")
  assert.EqualValues(t, a, b, "")
}

Error

函数类型:

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

ErrorAs

函数类型:

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

ErrorIs

函数类型:

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

逆断言

NotEqual/NotEqualValues

Assertions 对象

TestingTtestify*testing.T*AssertionsAssertionsTestingT
func TestEqual(t *testing.T) {
  assertions := assert.New(t)
  assertion.Equal(a, b, "")
  // ...
}
TestingT*testing.T
type TestingT interface{
  Errorf(format string, args ...interface{})
}

require

requireassertrequireassertfalse

mock

testify
package main
import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "net/http"
)
type User struct {
  Name string
  Age  int
}
type ICrawler interface {
  GetUserList() ([]*User, error)
}
type MyCrawler struct {
  url string
}
func (c *MyCrawler) GetUserList() ([]*User, error) {
  resp, err := http.Get(c.url)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  data, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    return nil, err
  }
  var userList []*User
  err = json.Unmarshal(data, &userList)
  if err != nil {
    return nil, err
  }
  return userList, nil
}
func GetAndPrintUsers(crawler ICrawler) {
  users, err := crawler.GetUserList()
  if err != nil {
    return
  }
  for _, u := range users {
    fmt.Println(u)
  }
}
Crawler.GetUserList()GetAndPrintUsers()ICrawlerICrawler
package main
import (
  "github.com/stretchr/testify/mock"
  "testing"
)
type MockCrawler struct {
  mock.Mock
}
func (m *MockCrawler) GetUserList() ([]*User, error) {
  args := m.Called()
  return args.Get(0).([]*User), args.Error(1)
}
var (
  MockUsers []*User
)
func init() {
  MockUsers = append(MockUsers, &User{"dj", 18})
  MockUsers = append(MockUsers, &User{"zhangsan", 20})
}
func TestGetUserList(t *testing.T) {
  crawler := new(MockCrawler)
  crawler.On("GetUserList").Return(MockUsers, nil)
  GetAndPrintUsers(crawler)
  crawler.AssertExpectations(t)
}
GetUserList()Mock.Called()Called()mock.ArgumentserrorInt()/String()/Bool()/Error()Get()interface{}
crawler.On("GetUserList").Return(MockUsers, nil)GetUserList()MockUsersnilGetUserList()Arguments.Get(0)Arguments.Error(1)
crawler.AssertExpectations(t)

运行:

$ go test
&{dj 18}
&{zhangsan 20}
PASS
ok      github.com/darjun/testify       0.258s
GetAndPrintUsers()
Times(n int)Once()/Twice()Hello(n int)
type IExample interface {
  Hello(n int) int
}
type Example struct {
}
func (e *Example) Hello(n int) int {
  fmt.Printf("Hello with %d\n", n)
  return n
}
func ExampleFunc(e IExample) {
  for n := 1; n <= 3; n++ {
    for i := 0; i <= n; i++ {
      e.Hello(n)
    }
  }
}

编写 Mock 对象:

type MockExample struct {
  mock.Mock
}
func (e *MockExample) Hello(n int) int {
  args := e.Mock.Called(n)
  return args.Int(0)
}
func TestExample(t *testing.T) {
  e := new(MockExample)
  e.On("Hello", 1).Return(1).Times(1)
  e.On("Hello", 2).Return(2).Times(2)
  e.On("Hello", 3).Return(3).Times(3)
  ExampleFunc(e)
  e.AssertExpectations(t)
}

运行:

$ go test

--- FAIL: TestExample (0.00s)
panic:
assert: mock: The method has been called over 1 times.
        Either do one more Mock.On("Hello").Return(...), or remove extra call.
        This call was unexpected:
                Hello(int)
                0: 1
        at: [equal_test.go:13 main.go:22] [recovered]

ExampleFunc()<=<
$ go test
PASS
ok      github.com/darjun/testify       0.236s

我们还可以设置以指定参数调用会导致 panic,测试程序的健壮性:

e.On("Hello", 100).Panic("out of range")

suite

testifyTestSuitetestifysuite.Suite
type SetupAllSuite interface {
  SetupSuite()
}
SetupSuite()SetupAllSuiteTearDownAllSuite
type TearDownAllSuite interface {
  TearDownSuite()
}
TearDonwSuite()TearDownSuite
type SetupTestSuite interface {
  SetupTest()
}
SetupTest()SetupTestSuiteTearDownTestSuite
type TearDownTestSuite interface {
  TearDownTest()
}
TearDownTest()TearDownTest
BeforeTest/AfterTest

我们来编写一个测试套件结构作为演示:

type MyTestSuit struct {
  suite.Suite
  testCount uint32
}
func (s *MyTestSuit) SetupSuite() {
  fmt.Println("SetupSuite")
}
func (s *MyTestSuit) TearDownSuite() {
  fmt.Println("TearDownSuite")
}
func (s *MyTestSuit) SetupTest() {
  fmt.Printf("SetupTest test count:%d\n", s.testCount)
}
func (s *MyTestSuit) TearDownTest() {
  s.testCount++
  fmt.Printf("TearDownTest test count:%d\n", s.testCount)
}
func (s *MyTestSuit) BeforeTest(suiteName, testName string) {
  fmt.Printf("BeforeTest suite:%s test:%s\n", suiteName, testName)
}
func (s *MyTestSuit) AfterTest(suiteName, testName string) {
  fmt.Printf("AfterTest suite:%s test:%s\n", suiteName, testName)
}
func (s *MyTestSuit) TestExample() {
  fmt.Println("TestExample")
}
go testTestXxxsuite.Run()
func TestExample(t *testing.T) {
  suite.Run(t, new(MyTestSuit))
}
suite.Run(t, new(MyTestSuit))MyTestSuitTestXxx
$ go test
SetupSuite
SetupTest test count:0
BeforeTest suite:MyTestSuit test:TestExample
TestExample
AfterTest suite:MyTestSuit test:TestExample
TearDownTest test count:1
TearDownSuite
PASS
ok      github.com/darjun/testify       0.375s

测试 HTTP 服务器

httptest
func index(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello World")
}
func greeting(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "welcome, %s", r.URL.Query().Get("name"))
}
func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)
  server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }
  if err := server.ListenAndServe(); err != nil {
    log.Fatal(err)
  }
}
httptestResponseRecorderhttp.ResponseWriter
func TestIndex(t *testing.T) {
  recorder := httptest.NewRecorder()
  request, _ := http.NewRequest("GET", "/", nil)
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)
  mux.ServeHTTP(recorder, request)
  assert.Equal(t, recorder.Code, 200, "get index error")
  assert.Contains(t, recorder.Body.String(), "Hello World", "body error")
}
func TestGreeting(t *testing.T) {
  recorder := httptest.NewRecorder()
  request, _ := http.NewRequest("GET", "/greeting", nil)
  request.URL.RawQuery = "name=dj"
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)
  mux.ServeHTTP(recorder, request)
  assert.Equal(t, recorder.Code, 200, "greeting error")
  assert.Contains(t, recorder.Body.String(), "welcome, dj", "body error")
}

运行:

$ go test
PASS
ok      github.com/darjun/go-daily-lib/testify/httptest 0.093s

很简单,没有问题。

recorder/muxsuite
type MySuite struct {
  suite.Suite
  recorder *httptest.ResponseRecorder
  mux      *http.ServeMux
}
func (s *MySuite) SetupSuite() {
  s.recorder = httptest.NewRecorder()
  s.mux = http.NewServeMux()
  s.mux.HandleFunc("/", index)
  s.mux.HandleFunc("/greeting", greeting)
}
func (s *MySuite) TestIndex() {
  request, _ := http.NewRequest("GET", "/", nil)
  s.mux.ServeHTTP(s.recorder, request)
  s.Assert().Equal(s.recorder.Code, 200, "get index error")
  s.Assert().Contains(s.recorder.Body.String(), "Hello World", "body error")
}
func (s *MySuite) TestGreeting() {
  request, _ := http.NewRequest("GET", "/greeting", nil)
  request.URL.RawQuery = "name=dj"
  s.mux.ServeHTTP(s.recorder, request)
  s.Assert().Equal(s.recorder.Code, 200, "greeting error")
  s.Assert().Contains(s.recorder.Body.String(), "welcome, dj", "body error")
}
TestXxx
func TestHTTP(t *testing.T) {
  suite.Run(t, new(MySuite))
}

总结

testifytestingassertmocksuite

参考

  • testify GitHub:github.com/stretchr/testify
  • Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib