Introduction to the

All is arguably the most popular Go test library (in terms of GitHub star count). All provides a number of convenient functions to help us output assert and error messages. Using the testing standard library, we need to write our own conditional judgments and output the corresponding information based on the results.

The core of the Trial has three parts:

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

The preparatory work

The code in this article uses Go Modules.

Create directory and initialize:

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

Install the Potency library:

$ go get -u github.com/stretchr/testify
Copy the code

assert

The Assert sublibrary provides convenient assertion functions that can greatly simplify writing test code. In general, it will need to judge the pattern of + information output before:

ifgot ! = expected { t.Errorf("Xxx failed expect:%d got:%d", got, expected)
}
Copy the code

Reduced to a single line of assertion code:

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

The structure is clearer and more readable. Developers familiar with other language testing frameworks should be familiar with the related use of Assert. In addition, the assert function automatically generates a clearer error description:

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

Use the same test file as testing. The test file is _test.go and the test function is TestXxx. Run tests with the go test command:

$ 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
Copy the code

We see information that’s easier to read.

All of the assert functions provided by the Trial function are available in two versions. The only difference between the two versions is that we need to specify at least two arguments, one format string, and several arguments, args:

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

In fact, Equal() is called inside the Equalf() function:

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...) ...). }Copy the code

So, we just need to focus on the version without f.

Contains

Function type:

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

Contains asserts that s Contains Contains. Where s can be string, array/slice, map. Accordingly, contains is the key of the substring, array/slice element, and map.

DirExists

Function type:

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

DirExists asserts that 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
Copy the code

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 be equal.

Empty

Function type:

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

Empty asserts that object is Empty. The meaning of Empty varies depending on the actual type stored in object:

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

EqualError

Function type:

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

EqualError asserts that theerror.error () returns the same value as errString.

EqualValues

Function type:

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

EqualValues asserts that expected is equal to actual or can be converted to the same type and equal. This condition is wider than Equal. If Equal() returns true, EqualValues() must also return true, and vice versa. At the heart of the implementation are 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 {
    // If 'ObjectsAreEqual' returns true, return it directly
  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) {
    // Try a type conversion
    return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
  }

  return false
}
Copy the code

For example, if I define a new type MyInt based on int, they both have a value of 100, Equal() will return false, and 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, "")}Copy the code

Error

Function type:

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

Error asserts that err is not nil.

ErrorAs

Function type:

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

ErrorAs asserts err that at least one of the error chains matches the target. This function is a wrapper around errors.as in the standard library.

ErrorIs

Function type:

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

ErrorIs asserts that err has target in the error chain.

Inverse assertion

The assertions above are their inverse assertions, such as NotEqual/NotEqualValues, etc.

Assertions object

Observe that all of the above assertions take TestingT as the first argument, which can be cumbersome when used in large quantities. All provides a convenient way to do that. T to create an * assertion object that defines all of the preceding Assertions, just don’t need to pass the TestingT parameter.

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

TestingT, by the way, is an interface that wraps *testing.T in a simple way:

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

require

Require provides the same interface as Assert, but when an error is encountered, require terminates the test directly, while assert returns false.

mock

Testify provides simple support for mocks. A Mock is simply the construction of a Mock object that provides the same interface as the original object and replaces it with the Mock object in the test. This way we can make the original object difficult to construct, especially involving external resources (databases, access networks, etc.). For example, we are now going to write a program that pulls user list information from a site and displays and analyzes it when the pull is complete. If you have to go to the network every time with great uncertainty, or even return a different list each time, this makes testing extremely difficult. We can use a Mock technique.

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(a) ([]*User, error) {
  resp, err := http.Get(c.url)
  iferr ! =nil {
    return nil, err
  }

  defer resp.Body.Close()
  data, err := ioutil.ReadAll(resp.Body)
  iferr ! =nil {
    return nil, err
  }

  var userList []*User
  err = json.Unmarshal(data, &userList)
  iferr ! =nil {
    return nil, err
  }

  return userList, nil
}

func GetAndPrintUsers(crawler ICrawler) {
  users, err := crawler.GetUserList()
  iferr ! =nil {
    return
  }

  for _, u := range users {
    fmt.Println(u)
  }
}
Copy the code

The crawler.getUserList () method completes the crawling and parsing, returning a list of users. To facilitate mocks, the GetAndPrintUsers() function accepts an 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(a) ([]*User, error) {
  args := m.Called()
  return args.Get(0).([]*User), args.Error(1)}var (
  MockUsers []*User
)

func init(a) {
  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)
}
Copy the code

To implement the GetUserList() method, you need to call the mock.called () method, passing in arguments (none in the example). Called() returns a mock.arguments object that holds the returned value. It provides the getmethod Int()/String()/Bool()/ error () for the basic type and error, and the generic getmethod Get(), which returns interface{} and requires a specific type assertion, and both take an argument that represents an index.

Crawler.on (“GetUserList”).return (MockUsers, nil) is where the Mock works the magic, indicating that the Return value of calling the GetUserList() method is MockUsers and nil, respectively, The return value is fetched by arguments.get (0) and arguments.error (1) in the GetUserList() method above.

The crawler. AssertExpectations (t) make assertions to Mock object.

Run:

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

The GetAndPrintUsers() function performs properly, and the list of users we provided with the Mock is correctly fetched.

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

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)
    }
  }
}
Copy the code

Write a 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)
}
Copy the code

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]
Copy the code

ExampleFunc() <= = = = = = = = = = = = = = =

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

We can also set it to specify that the argument call will cause panic, testing the robustness of the program:

e.On("Hello".100).Panic("out of range")
Copy the code

suite

All provides the functionality of the TestSuite, which is only a structure embedded with an anonymous suite.suite structure. A test suite can contain multiple tests that can share state, and hook methods can be defined to perform initialization and cleanup operations. Hooks are defined by interfaces, and the test suite structure that implements these interfaces calls the corresponding method when it runs to a specified node.

type SetupAllSuite interface {
  SetupSuite()
}
Copy the code

If a SetupSuite() method is defined (that is, the SetupAllSuite interface is implemented), call this method before all the tests in the suite start running. This corresponds to TearDownAllSuite:

type TearDownAllSuite interface {
  TearDownSuite()
}
Copy the code

If the TearDonwSuite() method is defined (that is, the TearDownSuite interface is implemented), it is called after all the tests in the suite have run.

type SetupTestSuite interface {
  SetupTest()
}
Copy the code

If a SetupTest() method is defined (that is, the SetupTestSuite interface is implemented), it is called before every test in the suite is executed. This corresponds to TearDownTestSuite:

type TearDownTestSuite interface {
  TearDownTest()
}
Copy the code

If the TearDownTest() method is defined (that is, the TearDownTest interface is implemented), it is called after the execution of each test in the suite.

There is also a pair of interfaces called BeforeTest/AfterTest, respectively, before and after each test run, taking the suite name and test name as arguments.

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

type MyTestSuit struct {
  suite.Suite
  testCount uint32
}

func (s *MyTestSuit) SetupSuite(a) {
  fmt.Println("SetupSuite")}func (s *MyTestSuit) TearDownSuite(a) {
  fmt.Println("TearDownSuite")}func (s *MyTestSuit) SetupTest(a) {
  fmt.Printf("SetupTest test count:%d\n", s.testCount)
}

func (s *MyTestSuit) TearDownTest(a) {
  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(a) {
  fmt.Println("TestExample")}Copy the code

Here we simply print information in each hook function to count the number of tests that have been executed. Since we are running with go test, we need to write a TestXxx function that calls suite.run () to Run the test suite:

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

Suite.run (t, new(MyTestSuit)) will Run all methods in MyTestSuit named TestXxx. 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
Copy the code

Testing the HTTP server

The Go standard library provides an HttpTest for testing HTTP servers. 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(a) {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)

  server := &http.Server{
    Addr:    ": 8080",
    Handler: mux,
  }

  iferr := server.ListenAndServe(); err ! =nil {
    log.Fatal(err)
  }
}
Copy the code

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 the response to the client. This allows us to pass objects of this type to handler functions. We then construct the server, pass in the object to drive the request processing, 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")}Copy the code

Run:

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

It’s simple, no problem.

But we found a problem, a lot of the above code has duplication, recorder/mux object creation, handler function registration. Using Suite we can centrally create and omit the repetitive code:

type MySuite struct {
  suite.Suite
  recorder *httptest.ResponseRecorder
  mux      *http.ServeMux
}

func (s *MySuite) SetupSuite(a) {
  s.recorder = httptest.NewRecorder()
  s.mux = http.NewServeMux()
  s.mux.HandleFunc("/", index)
  s.mux.HandleFunc("/greeting", greeting)
}

func (s *MySuite) TestIndex(a) {
  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(a) {
  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")}Copy the code

Finally, write a TestXxx driver test:

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

conclusion

All extends the Testing standard library, assert library, test surrogate Mock and test Suite, making it easier for us to write tests!

If you find a fun and useful Go library, please Go to GitHub and submit the issue😄

reference

  1. Testify GitHub:github.com/stretchr/te…
  2. Go daily GitHub: github.com/darjun/go-d…

I

My blog: darjun.github. IO

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