Go語言測試庫testify使用學習
簡介
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: TestEqual
FAIL
exit status 1
FAIL 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 int func 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 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()
函數接受一個ICrawler
接口。現在來定義我們的 Mock 對象,實現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) }
實現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} PASS ok 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 test PASS ok 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 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 服務器
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 test PASS ok 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
,讓我們編寫測試代碼更容易!
參考
- testify GitHub:github.com/stretchr/testify
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go語言測試庫testify使用學習的詳細內容,更多關於Go語言測試庫testify的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Go語言單元測試基礎從入門到放棄
- Go語言單元測試模擬服務請求和接口返回
- golang 對私有函數進行單元測試的實例
- Assert.assertEquals()方法參數詳解
- Go單元測試工具gomonkey的使用