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
- Testify GitHub:github.com/stretchr/te…
- 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 ~