Go語言測試庫testify使用學(xué)習
簡介
testify
可以說是最流行的(從 GitHub star 數(shù)來看)Go 語言測試庫了。testify
提供了很多方便的函數(shù)幫助我們做assert
和錯誤信息輸出。使用標準庫testing
,我們需要自己編寫各種條件判斷,根據(jù)判斷結(jié)果決定輸出對應(yīng)的信息。
testify
核心有三部分內(nèi)容:
assert
:斷言;mock
:測試替身;suite
:測試套件。
準備工作
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ 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
子庫提供了便捷的斷言函數(shù),可以大大簡化測試代碼的編寫??偟膩碚f,它將之前需要判斷 + 信息輸出的模式
:
if got != expected { t.Errorf("Xxx failed expect:%d got:%d", got, expected) }
簡化為一行斷言代碼:
assert.Equal(t, got, expected, "they should be equal")
結(jié)構(gòu)更清晰,更可讀。熟悉其他語言測試框架的開發(fā)者對assert
的相關(guān)用法應(yīng)該不會陌生。此外,assert
中的函數(shù)會自動生成比較清晰的錯誤描述信息:
func TestEqual(t *testing.T) { var a = 100 var b = 200 assert.Equal(t, a, b, "") }
使用testify
編寫測試代碼與testing
一樣,測試文件為_test.go
,測試函數(shù)為TestXxx
。使用go 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
我們看到信息更易讀。
testify
提供的assert
類函數(shù)眾多,每種函數(shù)都有兩個版本,一個版本是函數(shù)名不帶f
的,一個版本是帶f
的,區(qū)別就在于帶f
的函數(shù),我們需要指定至少兩個參數(shù),一個格式化字符串format
,若干個參數(shù)args
:
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})
實際上,在Equalf()
函數(shù)內(nèi)部調(diào)用了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...)...) }
所以,我們只需要關(guān)注不帶f
的版本即可。
Contains
函數(shù)類型:
func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool
Contains
斷言s
包含contains
。其中s
可以是字符串,數(shù)組/切片,map。相應(yīng)地,contains
為子串,數(shù)組/切片元素,map 的鍵。
DirExists
函數(shù)類型:
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool
DirExists
斷言路徑path
是一個目錄,如果path
不存在或者是一個文件,斷言失敗。
ElementsMatch
函數(shù)類型:
func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) bool
ElementsMatch
斷言listA
和listB
包含相同的元素,忽略元素出現(xiàn)的順序。listA/listB
必須是數(shù)組或切片。如果有重復(fù)元素,重復(fù)元素出現(xiàn)的次數(shù)也必須相等。
Empty
函數(shù)類型:
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
Empty
斷言object
是空,根據(jù)object
中存儲的實際類型,空的含義不同:
- 指針:
nil
; - 整數(shù):0;
- 浮點數(shù):0.0;
- 字符串:空串
""
; - 布爾:false;
- 切片或 channel:長度為 0。
EqualError
函數(shù)類型:
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
EqualError
斷言theError.Error()
的返回值與errString
相等。
EqualValues
函數(shù)類型:
func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
EqualValues
斷言expected
與actual
相等,或者可以轉(zhuǎn)換為相同的類型,并且相等。這個條件比Equal
更寬,Equal()
返回true
則EqualValues()
肯定也返回true
,反之則不然。實現(xiàn)的核心是下面兩個函數(shù),使用了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) { // 嘗試類型轉(zhuǎn)換 return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) } return false }
例如我基于int
定義了一個新類型MyInt
,它們的值都是 100,Equal()
調(diào)用將返回 false,EqualValues()
會返回 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
函數(shù)類型:
func Error(t TestingT, err error, msgAndArgs ...interface{}) bool
Error
斷言err
不為nil
。
ErrorAs
函數(shù)類型:
func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
ErrorAs
斷言err
表示的 error 鏈中至少有一個和target
匹配。這個函數(shù)是對標準庫中errors.As
的包裝。
ErrorIs
函數(shù)類型:
func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool
ErrorIs
斷言err
的 error 鏈中有target
。
逆斷言
上面的斷言都是它們的逆斷言,例如NotEqual/NotEqualValues
等。
Assertions 對象
觀察到上面的斷言都是以TestingT
為第一個參數(shù),需要大量使用時比較麻煩。testify
提供了一種方便的方式。先以*testing.T
創(chuàng)建一個*Assertions
對象,Assertions
定義了前面所有的斷言方法,只是不需要再傳入TestingT
參數(shù)了。
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 簡單來說就是構(gòu)造一個仿對象,仿對象提供和原對象一樣的接口,在測試中用仿對象來替換原對象。這樣我們可以在原對象很難構(gòu)造,特別是涉及外部資源(數(shù)據(jù)庫,訪問網(wǎng)絡(luò)等)。例如,我們現(xiàn)在要編寫一個從一個站點拉取用戶列表信息的程序,拉取完成之后程序顯示和分析。如果每次都去訪問網(wǎng)絡(luò)會帶來極大的不確定性,甚至每次返回不同的列表,這就給測試帶來了極大的困難。我們可以使用 Mock 技術(shù)。
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()
方法完成爬取和解析操作,返回用戶列表。為了方便 Mock,GetAndPrintUsers()
函數(shù)接受一個ICrawler
接口?,F(xiàn)在來定義我們的 Mock 對象,實現(xiàn)ICrawler
接口:
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) }
實現(xiàn)GetUserList()
方法時,需要調(diào)用Mock.Called()
方法,傳入?yún)?shù)(示例中無參數(shù))。Called()
會返回一個mock.Arguments
對象,該對象中保存著返回的值。它提供了對基本類型和error
的獲取方法Int()/String()/Bool()/Error()
,和通用的獲取方法Get()
,通用方法返回interface{}
,需要類型斷言為具體類型,它們都接受一個表示索引的參數(shù)。
crawler.On("GetUserList").Return(MockUsers, nil)
是 Mock 發(fā)揮魔法的地方,這里指示調(diào)用GetUserList()
方法的返回值分別為MockUsers
和nil
,返回值在上面的GetUserList()
方法中被Arguments.Get(0)
和Arguments.Error(1)
獲取。
最后crawler.AssertExpectations(t)
對 Mock 對象做斷言。
運行:
$ go test &{dj 18} &{zhangsan 20} PASS ok github.com/darjun/testify 0.258s
GetAndPrintUsers()
函數(shù)功能正常執(zhí)行,并且我們通過 Mock 提供的用戶列表也能正確獲取。
使用 Mock,我們可以精確斷言某方法以特定參數(shù)的調(diào)用次數(shù),Times(n int)
,它有兩個便捷函數(shù)Once()/Twice()
。下面我們要求函數(shù)Hello(n int)
要以參數(shù) 1 調(diào)用 1次,參數(shù) 2 調(diào)用兩次,參數(shù) 3 調(diào)用 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()
函數(shù)中<=
應(yīng)該是<
導(dǎo)致多調(diào)用了一次,修改過來繼續(xù)運行:
$ go test PASS ok github.com/darjun/testify 0.236s
我們還可以設(shè)置以指定參數(shù)調(diào)用會導(dǎo)致 panic,測試程序的健壯性:
e.On("Hello", 100).Panic("out of range")
suite
testify
提供了測試套件的功能(TestSuite
),testify
測試套件只是一個結(jié)構(gòu)體,內(nèi)嵌一個匿名的suite.Suite
結(jié)構(gòu)。測試套件中可以包含多個測試,它們可以共享狀態(tài),還可以定義鉤子方法執(zhí)行初始化和清理操作。鉤子都是通過接口來定義的,實現(xiàn)了這些接口的測試套件結(jié)構(gòu)在運行到指定節(jié)點時會調(diào)用對應(yīng)的方法。
type SetupAllSuite interface { SetupSuite() }
如果定義了SetupSuite()
方法(即實現(xiàn)了SetupAllSuite
接口),在套件中所有測試開始運行前調(diào)用這個方法。對應(yīng)的是TearDownAllSuite
:
type TearDownAllSuite interface { TearDownSuite() }
如果定義了TearDonwSuite()
方法(即實現(xiàn)了TearDownSuite
接口),在套件中所有測試運行完成后調(diào)用這個方法。
type SetupTestSuite interface { SetupTest() }
如果定義了SetupTest()
方法(即實現(xiàn)了SetupTestSuite
接口),在套件中每個測試執(zhí)行前都會調(diào)用這個方法。對應(yīng)的是TearDownTestSuite
:
type TearDownTestSuite interface { TearDownTest() }
如果定義了TearDownTest()
方法(即實現(xiàn)了TearDownTest
接口),在套件中每個測試執(zhí)行后都會調(diào)用這個方法。
還有一對接口BeforeTest/AfterTest
,它們分別在每個測試運行前/后調(diào)用,接受套件名和測試名作為參數(shù)。
我們來編寫一個測試套件結(jié)構(gòu)作為演示:
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") }
這里只是簡單在各個鉤子函數(shù)中打印信息,統(tǒng)計執(zhí)行完成的測試數(shù)量。由于要借助go test
運行,所以需要編寫一個TestXxx
函數(shù),在該函數(shù)中調(diào)用suite.Run()
運行測試套件:
func TestExample(t *testing.T) { suite.Run(t, new(MyTestSuit)) }
suite.Run(t, new(MyTestSuit))
會將運行MyTestSuit
中所有名為TestXxx
的方法。運行:
$ 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
測試 HTTP 服務(wù)器
Go 標準庫提供了一個httptest
用于測試 HTTP 服務(wù)器?,F(xiàn)在編寫一個簡單的 HTTP 服務(wù)器:
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
類型,它實現(xiàn)了http.ResponseWriter
接口,但是它只是記錄寫入的狀態(tài)碼和響應(yīng)內(nèi)容,不會發(fā)送響應(yīng)給客戶端。這樣我們可以將該類型的對象傳給處理器函數(shù)。然后構(gòu)造服務(wù)器,傳入該對象來驅(qū)動請求處理流程,最后測試該對象中記錄的信息是否正確:
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 test PASS ok github.com/darjun/go-daily-lib/testify/httptest 0.093s
很簡單,沒有問題。
但是我們發(fā)現(xiàn)一個問題,上面的很多代碼有重復(fù),recorder/mux
等對象的創(chuàng)建,處理器函數(shù)的注冊。使用suite
我們可以集中創(chuàng)建,省略這些重復(fù)的代碼:
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
驅(qū)動測試:
func TestHTTP(t *testing.T) { suite.Run(t, new(MySuite)) }
總結(jié)
testify
擴展了testing
標準庫,斷言庫assert
,測試替身mock
和測試套件suite
,讓我們編寫測試代碼更容易!
參考
- testify GitHub:github.com/stretchr/testify
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go語言測試庫testify使用學(xué)習的詳細內(nèi)容,更多關(guān)于Go語言測試庫testify的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言題解LeetCode724尋找數(shù)組的中心下標
這篇文章主要為大家介紹了Go語言題解LeetCode724尋找數(shù)組的中心下標,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12Go?Wails開發(fā)桌面應(yīng)用使用示例探索
這篇文章主要為大家介紹了Go?Wails的使用示例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12go語言實現(xiàn)簡易比特幣系統(tǒng)之交易簽名及校驗功能
這篇文章主要介紹了go語言實現(xiàn)簡易比特幣系統(tǒng)之交易簽名及校驗功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04解決Goland 提示 Unresolved reference 錯誤的問題
這篇文章主要介紹了解決Goland 提示 Unresolved reference 錯誤的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12