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
- testify GitHub:github.com/stretchr/testify
- 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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。