简介
testify
能够说是最风行的(从 GitHub star 数来看)Go 语言测试库了。testify
提供了很多不便的函数帮忙咱们做assert
和错误信息输入。应用规范库testing
,咱们须要本人编写各种条件判断,依据判断后果决定输入对应的信息。
testify
外围有三局部内容:
assert
:断言;mock
:测试替身;suite
:测试套件。
筹备工作
本文代码应用 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")
构造更清晰,更可读。相熟其余语言测试框架的开发者对assert
的相干用法应该不会生疏。此外,assert
中的函数会主动生成比拟清晰的谬误形容信息:
func TestEqual(t *testing.T) { var a = 100 var b = 200 assert.Equal(t, a, b, "")}
应用testify
编写测试代码与testing
一样,测试文件为_test.go
,测试函数为TestXxx
。应用go test
命令运行测试:
$ go test--- FAIL: TestEqual (0.00s) assert_test.go:12: Error Trace: Error: Not equal: expected: 100 actual : 200 Test: TestEqualFAILexit status 1FAIL github.com/darjun/go-daily-lib/testify/assert 0.107s
咱们看到信息更易读。
testify
提供的assert
类函数泛滥,每种函数都有两个版本,一个版本是函数名不带f
的,一个版本是带f
的,区别就在于带f
的函数,咱们须要指定至多两个参数,一个格式化字符串format
,若干个参数args
:
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
Contains
断言s
蕴含contains
。其中s
能够是字符串,数组/切片,map。相应地,contains
为子串,数组/切片元素,map 的键。
DirExists
函数类型:
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool
DirExists
断言门路path
是一个目录,如果path
不存在或者是一个文件,断言失败。
ElementsMatch
函数类型:
func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) bool
ElementsMatch
断言listA
和listB
蕴含雷同的元素,疏忽元素呈现的程序。listA/listB
必须是数组或切片。如果有反复元素,反复元素呈现的次数也必须相等。
Empty
函数类型:
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
Empty
断言object
是空,依据object
中存储的理论类型,空的含意不同:
- 指针:
nil
; - 整数:0;
- 浮点数:0.0;
- 字符串:空串
""
; - 布尔:false;
- 切片或 channel:长度为 0。
EqualError
函数类型:
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
EqualError
断言theError.Error()
的返回值与errString
相等。
EqualValues
函数类型:
func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
EqualValues
断言expected
与actual
相等,或者能够转换为雷同的类型,并且相等。这个条件比Equal
更宽,Equal()
返回true
则EqualValues()
必定也返回true
,反之则不然。实现的外围是上面两个函数,应用了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}
例如我基于int
定义了一个新类型MyInt
,它们的值都是 100,Equal()
调用将返回 false,EqualValues()
会返回 true:
type MyInt intfunc 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
Error
断言err
不为nil
。
ErrorAs
函数类型:
func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
ErrorAs
断言err
示意的 error 链中至多有一个和target
匹配。这个函数是对规范库中errors.As
的包装。
ErrorIs
函数类型:
func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool
ErrorIs
断言err
的 error 链中有target
。
逆断言
下面的断言都是它们的逆断言,例如NotEqual/NotEqualValues
等。
Assertions 对象
察看到下面的断言都是以TestingT
为第一个参数,须要大量应用时比拟麻烦。testify
提供了一种不便的形式。先以*testing.T
创立一个*Assertions
对象,Assertions
定义了后面所有的断言办法,只是不须要再传入TestingT
参数了。
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
require
提供了和assert
同样的接口,然而遇到谬误时,require
间接终止测试,而assert
返回false
。
mock
testify
提供了对 Mock 的简略反对。Mock 简略来说就是结构一个仿对象,仿对象提供和原对象一样的接口,在测试中用仿对象来替换原对象。这样咱们能够在原对象很难结构,特地是波及内部资源(数据库,拜访网络等)。例如,咱们当初要编写一个从一个站点拉取用户列表信息的程序,拉取实现之后程序显示和剖析。如果每次都去拜访网络会带来极大的不确定性,甚至每次返回不同的列表,这就给测试带来了极大的艰难。咱们能够应用 Mock 技术。
package mainimport ( "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()
办法实现爬取和解析操作,返回用户列表。为了不便 Mock,GetAndPrintUsers()
函数承受一个ICrawler
接口。当初来定义咱们的 Mock 对象,实现ICrawler
接口:
package mainimport ( "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.Arguments
对象,该对象中保留着返回的值。它提供了对根本类型和error
的获取办法Int()/String()/Bool()/Error()
,和通用的获取办法Get()
,通用办法返回interface{}
,须要类型断言为具体类型,它们都承受一个示意索引的参数。
crawler.On("GetUserList").Return(MockUsers, nil)
是 Mock 施展魔法的中央,这里批示调用GetUserList()
办法的返回值别离为MockUsers
和nil
,返回值在下面的GetUserList()
办法中被Arguments.Get(0)
和Arguments.Error(1)
获取。
最初crawler.AssertExpectations(t)
对 Mock 对象做断言。
运行:
$ go test&{dj 18}&{zhangsan 20}PASSok github.com/darjun/testify 0.258s
GetAndPrintUsers()
函数性能失常执行,并且咱们通过 Mock 提供的用户列表也能正确获取。
应用 Mock,咱们能够准确断言某办法以特定参数的调用次数,Times(n int)
,它有两个便捷函数Once()/Twice()
。上面咱们要求函数Hello(n int)
要以参数 1 调用 1次,参数 2 调用两次,参数 3 调用 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) } }}
编写 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 testPASSok github.com/darjun/testify 0.236s
咱们还能够设置以指定参数调用会导致 panic,测试程序的健壮性:
e.On("Hello", 100).Panic("out of range")
suite
testify
提供了测试套件的性能(TestSuite
),testify
测试套件只是一个构造体,内嵌一个匿名的suite.Suite
构造。测试套件中能够蕴含多个测试,它们能够共享状态,还能够定义钩子办法执行初始化和清理操作。钩子都是通过接口来定义的,实现了这些接口的测试套件构造在运行到指定节点时会调用对应的办法。
type SetupAllSuite interface { SetupSuite()}
如果定义了SetupSuite()
办法(即实现了SetupAllSuite
接口),在套件中所有测试开始运行前调用这个办法。对应的是TearDownAllSuite
:
type TearDownAllSuite interface { TearDownSuite()}
如果定义了TearDonwSuite()
办法(即实现了TearDownSuite
接口),在套件中所有测试运行实现后调用这个办法。
type SetupTestSuite interface { SetupTest()}
如果定义了SetupTest()
办法(即实现了SetupTestSuite
接口),在套件中每个测试执行前都会调用这个办法。对应的是TearDownTestSuite
:
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 test
运行,所以须要编写一个TestXxx
函数,在该函数中调用suite.Run()
运行测试套件:
func TestExample(t *testing.T) { suite.Run(t, new(MyTestSuit))}
suite.Run(t, new(MyTestSuit))
会将运行MyTestSuit
中所有名为TestXxx
的办法。运行:
$ go testSetupSuiteSetupTest test count:0BeforeTest suite:MyTestSuit test:TestExampleTestExampleAfterTest suite:MyTestSuit test:TestExampleTearDownTest test count:1TearDownSuitePASSok github.com/darjun/testify 0.375s
测试 HTTP 服务器
Go 规范库提供了一个httptest
用于测试 HTTP 服务器。当初编写一个简略的 HTTP 服务器:
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) }}
很简略。httptest
提供了一个ResponseRecorder
类型,它实现了http.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 testPASSok github.com/darjun/go-daily-lib/testify/httptest 0.093s
很简略,没有问题。
然而咱们发现一个问题,下面的很多代码有反复,recorder/mux
等对象的创立,处理器函数的注册。应用suite
咱们能够集中创立,省略这些反复的代码:
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))}
总结
testify
扩大了testing
规范库,断言库assert
,测试替身mock
和测试套件suite
,让咱们编写测试代码更容易!
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue
参考
- testify GitHub:github.com/stretchr/testify
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~