從入門到深入 Go 我們已經走了很長的路,當你想啟動多個測試類的時候你是不是想啟動多個 main 方法,但是 Go 限制了在同一個 package 下只能有一個 main,所以這條路你是走不通的。那我們想寫單元測試的時候應該如何操作呢?別着急,不用引入任何的第三方包,單元測試 Go 也有默認的規范寫法。
約定
在 Go SDK 中 ”testing“ 包的內容就是 Go 默認提供的單元測試支持。Go 標准庫對單元測試編寫的格式有一些硬性要求:
- 所有測試方法必須放在位於以 _test.go 結尾的文件中,這樣在執行 go build 構建的時候測試代碼才會被排除。
- 測試函數的命名必須以 Test 開頭,並且跟在 Test 后面的后綴開頭第一個字母必須大寫。
- 測試方法必須要包含 “t *testing.T” 參數。
func TestGetUser(t *testing.T)
func TestInsert(t *testing.T)
其中參數 t 用於報告測試失敗和附加的日志信息。 testing.T 的擁有的方法如下:
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
比如我們現在有一段回文檢測的 func:
package service
// 判斷一個字符串s是否時回文字符串
func IsPalindrome(s string) bool {
for i := range s {
if s[i] != s[len(s)-1-i] {
return false
}
}
return true
}
想在單元測試中調用 這個方法:
package demo
import (
"gorm-demo/service"
"testing"
)
func TestString(t *testing.T) {
palindrome := service.IsPalindrome("3ee3")
if palindrome {
t.Logf("IsPalindrome test success, param=%s", "3e1e3")
} else {
t.Fatalf("IsPalindrome test fail, param=%s", "3e1e3")
}
}
根據是否是回文輸出對應的結果:
=== RUN TestString
string_test.go:11: IsPalindrome test success, param=3e1e3
--- PASS: TestString (0.00s)
PASS
除了直接執行對應的測試方法之外我們還可以通過 go test 命令行的方式來執行測試,go test 是 Go 語言自帶的測試工具,其中包含的是兩類:單元測試和性能測試,通過 go help test 可以看到 go test 的使用說明:
格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]
參數解讀:
-c : 編譯go test成為可執行的二進制文件,但是不運行測試。
-i : 安裝測試包依賴的package,但是不運行測試。
關於build flags,調用go help build,這些是編譯運行過程中需要使用到的參數,一般設置為空
關於packages,調用go help packages,這些是關於包的管理,一般設置為空
關於flags for test binary,調用go help testflag,這些是go test過程中經常使用到的參數
-test.v : 是否輸出全部的單元測試用例(不管成功或者失敗),默認沒有加上,所以只輸出失敗的單元測試用例。
-test.run pattern: 只跑哪些單元測試用例
-test.bench patten: 只跑那些性能測試用例
-test.benchmem : 是否在性能測試的時候輸出內存情況
-test.benchtime t : 性能測試運行的時間,默認是1s
-test.cpuprofile cpu.out : 是否輸出cpu性能分析文件
-test.memprofile mem.out : 是否輸出內存性能分析文件
-test.blockprofile block.out : 是否輸出內部goroutine阻塞的性能分析文件
-test.memprofilerate n : 內存性能分析的時候有一個分配了多少的時候才打點記錄的問題。這個參數就是設置打點的內存分配間隔,也就是profile中一個sample代表的內存大小。默認是設置為512 * 1024的。如果你將它設置為1,則每分配一個內存塊就會在profile中有個打點,那么生成的profile的sample就會非常多。如果你設置為0,那就是不做打點了。
你可以通過設置memprofilerate=1和GOGC=off來關閉內存回收,並且對每個內存塊的分配進行觀察。
-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞時候打點的納秒數。默認不設置就相當於-test.blockprofilerate=1,每一納秒都打點記錄一下
-test.parallel n : 性能測試的程序並行cpu數,默認等於GOMAXPROCS。
-test.timeout t : 如果測試用例運行時間超過t,則拋出panic
-test.cpu 1,2,4 : 程序運行在哪些CPU上面,使用二進制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一個道理
-test.short : 將那些運行時間較長的測試用例運行時間縮短
測試覆蓋率
Go提供內置功能來檢查你的代碼覆蓋率。我們可以使用 go test -cover 來查看測試覆蓋率。
MacBook-Pro:mockDemo yy$ go test -cover
PASS
coverage: 0.0% of statements
ok gorm-demo/test/mockDemo 0.007s
Go還提供了一個額外的-coverprofile參數,用來將覆蓋率相關的記錄信息輸出到一個文件。例如:
MacBook-Pro:mockDemo yy$ go test -cover -coverprofile=tt.log
PASS
coverage: 0.0% of statements
ok gorm-demo/test/mockDemo 0.007s
生成 tt.log 文件之后,執行 go tool cover -html=tt.log,使用 cover 工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個 HTML 報告。
斷言
使用 Java 的同學看到這里估計會問: Go 中沒有斷言嗎?還需要自己去判斷。
其實沒有斷言這種東西我們仔細想想也並不難理解,從 Go 的 error 包設計將異常作為返回值而不是使用 try-catch 的模式來說,Go 希望你在測試階段就知曉每一個可能出現的異常,而不是將異常吞掉。所以 Assert 這種吞掉錯誤的功能 Go 官方也不想提供。
當然 Go 官方不提供不代表廣大開發同胞真的不想用,這不有大哥開發了靈活又好用的斷言庫 testify ,有了它,我們上面的代碼就可以改為這樣:
assert.True(t, service.IsPalindrome("3e45e3"))
輸出:
=== RUN TestString
string_test.go:11:
Error Trace: string_test.go:11
Error: Should be true
Test: TestString
--- FAIL: TestString (0.00s)
FAIL
簡介明了,一眼就知道測試用例是否通過。真的是誰用誰知道。
具體 testify 還有很多實用的斷言方法:
// 判斷兩個值是否相等
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
// 判斷兩個值不相等
func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
// 測試失敗,測試中斷
func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool
// 判斷值是否為nil,常用於 error 的判斷
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
// 判斷值是否不為nil,常用於 error 的判斷
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
大家有興趣可以看看 API。
Mock 功能
使用這個功能之前,先着重聲明 mock 的意思。
mock 模擬,模仿的意思。這里這里提供的功能是模擬某段功能,用我們的模擬邏輯去代替。
testify 也支持 Mock,不過 Go 原生的 mock 框架就挺好的。GoMock 是由 Go 官方開發維護的測試框架,實現了較為完整的基於 interface 的 Mock 功能。注意它沒在 SDK 里面哈。
go get -u github.com/golang/mock/gomock
Gomock 還提供了 mockgen 工具用來輔助生成測試代碼。
go get -u github.com/golang/mock/mockgen
使用的時候這兩個包都需要安裝。
安裝 mockgen 有兩種方式,你可以只在你的當前代碼目錄執行 go get ,這樣 mockgen 命令只對當前目錄有效;或者你直接取 mockgen 的目錄下執行 go build ,編譯后會在這個目錄下生成一個可執行程序 mockgen。然后將這個可執行程序 mockgen 拖到 $GOPATH/bin/ 目錄下后面你就可以全局使用 mockgen 。
mockgen 使用也很簡單,可以對包或者源代碼文件生成指定接口的 Mock 代碼,注意是對接口文件哈。
package mockDemo
type Task interface {
Do(string) (bool, error)
}
想對指定接口生成 mock 代碼使用如下命令:
mockgen -source=源文件路徑 -destination=寫入文件的路徑(沒有這個參數輸出到終端) -package=生成文件的包名
demo :
mockgen -source=/Users/cc/go/src/go-web-demo/test/mockDemo/task.go -destination=/Users/cc/go/src/go-web-demo/test/mockDemo/mock_task_test.go -package=mockDemo
-source:設置需要模擬(mock)的接口文件
-destination:設置 mock 文件輸出的地方,若不設置則打印到標准輸出中
-package:設置 mock 文件的包名,若不設置則為 `mock_` 前綴加上文件名(如本文的包名會為 mock_person)
接下來上示例,再次解釋 mock 就是要模擬,比如我們的 Do 方法要去連接數據庫查詢數據,這里因為不方便測試連接數據庫這段代碼,但是又不想影響整體測試流程所以用 mock 的方式去替代這段邏輯。解釋清楚了我們上代碼。
整體測試代碼如下:
接口:
package mockDemo
type Task interface {
Do(string) (bool, error)
}
根據該接口生成 mock 類:
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/yangyue/go/src/go-web-demo/test/mockDemo/task.go
// Package mockDemo is a generated GoMock package.
package mockDemo
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockTask is a mock of Task interface.
type MockTask struct {
ctrl *gomock.Controller
recorder *MockTaskMockRecorder
}
// MockTaskMockRecorder is the mock recorder for MockTask.
type MockTaskMockRecorder struct {
mock *MockTask
}
// NewMockTask creates a new mock instance.
func NewMockTask(ctrl *gomock.Controller) *MockTask {
mock := &MockTask{ctrl: ctrl}
mock.recorder = &MockTaskMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTask) EXPECT() *MockTaskMockRecorder {
return m.recorder
}
// Do mocks base method.
func (m *MockTask) Do(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do.
func (mr *MockTaskMockRecorder) Do(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockTask)(nil).Do), arg0)
}
測試方法:
package mockDemo
import (
"fmt"
"github.com/golang/mock/gomock"
"testing"
)
func TestMock(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
task := NewMockTask(ctl)
gomock.InOrder(task.EXPECT().Do("banana").Return(true, nil))
task.Do("banana")
}
gomock.NewController:返回 gomock.Controller
,它代表 mock 生態系統中的頂級控件。定義了 mock 對象的范圍、生命周期和期待值。多 goroutine 下是線程安全的。
NewMockTask() 創建一個新的 MockTask 實例,因為 MockTask 實現了 Task 接口所有后面實際是調用 MockTask 的實現方法。
gomock.InOrder(calls ...*Call):聲明調用 Call 的順序,這里可以傳入多個 Call。
task.EXPECT().Do("banana").Return(true, nil):EXPECT() 是期望拿到返回值,Call 的方法調用類似於 Java 中的 Build 模式,鏈式調用。有如下方法可供使用:
- Call.Do():聲明在匹配時要運行的操作
- Call.DoAndReturn():聲明在匹配調用時要運行的操作,並且模擬返回該函* 數的返回值
- Call.MaxTimes():設置最大的調用次數為 n 次
- Call.MinTimes():設置最小的調用次數為 n 次
- Call.AnyTimes():允許調用次數為 0 次或更多次
- Call.Times():設置調用次數為 n 次
我們測試一下調用順序檢測,多個 Call 的情況:
package mockDemo
import (
"github.com/golang/mock/gomock"
"testing"
)
func TestMock(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
task := NewMockTask(ctl)
call1 := task.EXPECT().Do("banana").Return(true, nil)
call2 := task.EXPECT().Do("apple").Return(true, nil)
call3 := task.EXPECT().Do("pineapple").Return(true, nil)
gomock.InOrder(call1, call2, call3)
task.Do("apple")
task.Do("banana")
task.Do("pineapple")
}
順序不一樣的情況下是會報錯的。
總結一下 mock 的使用:mock 是面向接口的測試,當你想測試的邏輯只是一段獨立功能性的代碼而沒有提供接口去抽象化的時候你無法使用 mock 功能。當然不是說必須要面向接口開發,有接口的定義會更加規范化你的代碼讓你知道寫出來的邏輯是審慎總結的。