Introduction

testify can be said to be the most popular (from GitHub star count) Go language testing library. testify provides a lot of convenient functions to help us do assert and error message output. Using the standard library testing , we need to write various condition judgments ourselves, and decide to output the corresponding information according to the judgment results.

testify core of 06114729f52189 has three parts:

  • assert : assertion;
  • mock : test double;
  • suite : Test suite.

Ready to work

The code in this article uses Go Modules.

Create a directory and initialize:

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

Install the testify library:

$ go get -u github.com/stretchr/testify

assert

assert sub-library provides convenient assertion function, which can greatly simplify the writing of test code. In general, it will need judgment + information output mode before:

if got != expected {
  t.Errorf("Xxx failed expect:%d got:%d", got, expected)
}

Simplified to a line of assertion code:

assert.Equal(t, got, expected, "they should be equal")

The structure is clearer and more readable. Developers familiar with other language testing frameworks should be familiar with the related usage assert In addition, assert will automatically generate clearer error description information:

func TestEqual(t *testing.T) {
  var a = 100
  var b = 200
  assert.Equal(t, a, b, "")
}

Using testify write test code is the testing , the test file is _test.go , and the test function is TestXxx . Use the go test command to run the 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

We see that the information is easier to read.

testify provides assert . Each function has two versions. One version is the function name without f , and the other version is with f . The difference is that f , we need to specify at least two parameters. A format string format , several parameters args :

func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})

In fact, Equalf() is called inside the function 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...)...)
}

Therefore, we only need to pay attention to the version f

Contains

Function type:

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

Contains asserts that s contains contains . Where s can be string, array/slice, map. Correspondingly, contains is a substring, an array/slice element, and a map key.

DirExists

Function type:

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

DirExists asserts that the path path is a directory. If path does not exist or is a file, the assertion fails.

ElementsMatch

Function type:

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

ElementsMatch asserts that listA and listB contain the same elements, ignoring the order in which the elements appear. listA/listB must be an array or slice. If there are repeated elements, the number of repeated elements must also be equal.

Empty

Function type:

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

Empty asserts that object is empty. According to object , the meaning of empty is different:

  • Pointer: nil ;
  • Integer: 0;
  • Floating point number: 0.0;
  • String: empty string "" ;
  • Boolean: false;
  • Slice or channel: length is 0.

EqualError

Function type:

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

EqualError assertion theError.Error() return value and errString equal.

EqualValues

Function type:

func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool

EqualValues asserts that expected is actual , or can be converted to the same type and equal. This condition is wider Equal Equal() returns true then EqualValues() must also return true , and vice versa. The core of the implementation is the following two functions, using reflect.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
}

For example, I defined a new type MyInt int , their values are all 100, the Equal() will return false, and the call of EqualValues() will return true:

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

Function type:

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

Error asserts that err not nil .

ErrorAs

Function type:

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

ErrorAs asserted err error chain represented by at least one and target match. This function is a wrapper errors.As

ErrorIs

Function type:

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

ErrorIs assertion err the error chain has target .

Inverse assertion

The above assertions are their inverse assertions, such as NotEqual/NotEqualValues and so on.

Assertions object

Observe that the above assertions are all with TestingT as the first parameter, which is troublesome when a lot of usage is required. testify provides a convenient way. First to *testing.T create a *Assertions objects, Assertions defines all previous assertions, but do not need to pass TestingT parameters of.

func TestEqual(t *testing.T) {
  assertions := assert.New(t)
  assertion.Equal(a, b, "")
  // ...
}

By the way, TestingT is an interface, and a simple package is made *testing.T

type TestingT interface{
  Errorf(format string, args ...interface{})
}

require

require provides the assert , but when an error is encountered, require directly terminates the test, and assert returns false .

mock

testify provides simple support for Mock. Mock is simply to construct a imitation object , the imitation object provides the same interface as the original object, and the imitation object is used to replace the original object in the test. In this way, we can be difficult to construct in the original object, especially involving external resources (database, access to the network, etc.). For example, we are now going to write a program to pull user list information from a site, and the program will display and analyze after the pull is complete. If you visit the network every time, it will bring great uncertainty, and even return a different list every time, which brings great difficulties to the test. We can use Mock technology.

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() method completes the crawling and parsing operations, and returns the user list. To facilitate mocking, the GetAndPrintUsers() function accepts a ICrawler interface. Now let's define our Mock object to implement the ICrawler interface:

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)
}

When implementing the GetUserList() method, you need to call the Mock.Called() method and pass in the parameters (there are no parameters in the example). Called() will return a mock.Arguments object, which holds the returned value. It provides the access method Int()/String()/Bool()/Error() for the basic type and error , and the general access method Get() . The general method returns interface{} . It requires the type assertion to be a specific type, and they all accept a parameter indicating an index.

crawler.On("GetUserList").Return(MockUsers, nil) is the place where Mock plays its magic. Here it indicates that the return values of GetUserList() MockUsers and nil respectively, and the return value is GetUserList() by Arguments.Get(0) and Arguments.Error(1) in the above 06114729f52a1f method.

Finally, crawler.AssertExpectations(t) makes an assertion on the Mock object.

run:

$ go test
&{dj 18}
&{zhangsan 20}
PASS
ok      github.com/darjun/testify       0.258s

GetAndPrintUsers() function is executed normally, and the user list provided by Mock can also be obtained correctly.

Using Mock, we can accurately assert the number of times a method is called with specific parameters, Times(n int) , which has two convenience functions Once()/Twice() . Below we require the function Hello(n int) to be called once with parameter 1, parameter 2 twice, and parameter 3 three times:

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)
    }
  }
}

Write the Mock object:

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)
}

run:

$ 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]

It turns out that ExampleFunc() function <= should be < caused one more call. Modify it to continue running:

$ go test
PASS
ok      github.com/darjun/testify       0.236s

We can also set to specify the parameters to call will lead to panic, test the robustness of the program:

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

suite

testify provides the function of the test suite ( TestSuite ). The testify test suite is just a structure with an anonymous suite.Suite structure embedded in it. The test suite can contain multiple tests, they can share state, and hook methods can be defined to perform initialization and cleanup operations. Hooks are defined through interfaces, and the test suite structure that implements these interfaces will call the corresponding method when it runs to the specified node.

type SetupAllSuite interface {
  SetupSuite()
}

If the SetupSuite() method is defined (that is, the SetupAllSuite interface is implemented), call this method before all the tests in the suite start to run. The corresponding is TearDownAllSuite :

type TearDownAllSuite interface {
  TearDownSuite()
}

If the TearDonwSuite() method is defined (that is, the TearDownSuite interface is implemented), call this method after all the tests in the suite are run.

type SetupTestSuite interface {
  SetupTest()
}

If the SetupTest() method is defined (that is, the SetupTestSuite interface is implemented), this method will be called before each test in the suite is executed. The corresponding is TearDownTestSuite :

type TearDownTestSuite interface {
  TearDownTest()
}

If the TearDownTest() method is defined (that is, the TearDownTest interface is implemented), this method will be called after each test in the suite is executed.

There is also a pair of interfaces BeforeTest/AfterTest , which are called before/after each test run and accept the suite name and test name as parameters.

Let's write a test suite structure as a demonstration:

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")
}

Here is simply printing information in each hook function to count the number of completed tests. Because you need to go test , you need to write a TestXxx function, in which you call suite.Run() run the test suite:

func TestExample(t *testing.T) {
  suite.Run(t, new(MyTestSuit))
}

suite.Run(t, new(MyTestSuit)) will run MyTestSuit all named TestXxx methods. run:

$ 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

Test the HTTP server

The Go standard library provides a httptest for testing HTTP server. Now write a simple HTTP server:

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)
  }
}

It's very simple. httptest provides a ResponseRecorder type, which implements the http.ResponseWriter interface, but it only records the written status code and response content, and does not send a response to the client. In this way, we can pass this type of object to the handler function. Then construct the server, pass in the object to drive the request processing flow, and finally test whether the information recorded in the object is correct:

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")
}

run:

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

It's simple, no problem.

But we found a problem. Many of the above codes are duplicated, the recorder/mux objects such as 06114729f52dc5, and the registration of handler functions. Using suite we can focus on creating, omitting these repeated codes:

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")
}

Finally, write a TestXxx driver test:

func TestHTTP(t *testing.T) {
  suite.Run(t, new(MySuite))
}

Summarize

testify extends the testing standard library, the assertion library assert , the test mock and the test suite suite , making it easier for us to write test codes!

If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

refer to

  1. testify GitHub:github.com/stretchr/testify
  2. Go daily library GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望356 粉丝