通過閱讀上一篇文章,相信你對怎麼做單元測試已經有了初步的概念,可以著手對現有的專案進行改造並開展測試了。學會了走路,我們嘗試跑起來,本篇主要介紹gomock測試框架,讓我們的單元測試更加有效率。
表格驅動測試方法(Table Driven Tests)
當針對某方法進行單元測試的時候,通常不止寫一個測試用例,我們需要測試該方法在多種入參條件下是否都能正常工作,特別是要針對邊界值進行測試。通常這個時候表格驅動測試就派上用場了——當你發現你在寫測試方法的時候用上了複製貼上,這就說明你需要考慮使用表格驅動測試來構建你的測試方法了。我們依舊來舉個例子:
func TestTime(t *testing.T) {
testCases := []struct { // 設計我們的測試用例
gmt string
loc string
want string
}{
{"12:31", "Europe/Zuri", "13:31"}, // incorrect location name
{"12:31", "America/New_York", "7:31"}, // should be 07:31
{"08:08", "Australia/Sydney", "18:08"},
}
for _, tc := range testCases { // 迴圈執行測試用例
loc, err := time.LoadLocation(tc.loc)
if err != nil {
t.Fatalf("could not load location %q", tc.loc)
}
gmt, _ := time.Parse("15:04", tc.gmt)
if got := gmt.In(loc).Format("15:04"); got != tc.want {
t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
}
}
}
複製程式碼
表格驅動測試方法讓我們的測試方法更加清晰和簡練,減少了複製貼上,並大大提高的測試程式碼的可讀性。
還記得上文說單元測試也是需要維護的嗎?單元測試也是程式碼的一部分,也應當被認真對待。記得要用表格驅動測試的方法來組織你的測試用例,同時別忘了像正式程式碼那樣,寫上相應的註釋。 更多參考: github.com/golang/go/w… blog.golang.org/subtests
使用測試框架——gomock
What is gomock?
gomock是Google開源的golang測試框架。或者引用官方的話來說:“GoMock is a mocking framework for the Go programming language”。
Why gomock?
上篇文章末尾介紹了mock和stub相結合的測試方法,可以感受到mock與stub結合起來功能固然強大——呼叫順序檢測,呼叫次數檢測,動態控制函式的返回值等等,但同時,其帶來的維護成本和複雜度缺是不可忽視的,手動維護這樣一套測試程式碼那將是一場災難。我們期望能用一套框架或者工具,在提供強大的測試功能的同時幫我們維護複雜的mock程式碼。
How does it work?
gomock通過mockgen
命令生成包含mock物件的.go
檔案,其生成的mock物件具備mock+stub的強大功能,並將我們從寫mock物件中解放了出來:
mockgen -destination foo_mock.go -source foo.go -package foo //mock foo.go裡面所有的介面,將mock結果儲存到foo_mock.go
複製程式碼
gomock讓我們既能使用mock與stub結合的強大功能,又不需要手動維護這些mock物件,豈不美哉?
舉個例子
在這裡我們對gomock的基本功能做一個簡單演示:
假設我們的介面定義在user.go
:
// user.go
package user
// User 表示一個使用者
type User struct {
Name string
}
// UserRepository 使用者倉庫
type UserRepository interface {
// 根據使用者id查詢得到一個使用者或是錯誤資訊
FindOne(id int) (*User,error)
}
複製程式碼
通過mockgen在同目錄下生成mock檔案user_mock.go
mockgen -source user.go -destination user_mock.go -package user
複製程式碼
然後在該目錄下新建user_test.go
來寫我們的測試函式,上述步驟完成之後,我們的目錄結構如下:
└── user
├── user.go
├── user_mock.go
└── user_test.go
複製程式碼
設定函式的返回值
// 靜態設定返回值
func TestReturn(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 期望FindOne(1)返回張三使用者
repo.EXPECT().FindOne(1).Return(&User{Name: "張三"}, nil)
// 期望FindOne(2)返回李四使用者
repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
// 期望給FindOne(3)返回找不到使用者的錯誤
repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
// 驗證一下結果
log.Println(repo.FindOne(1)) // 這是張三
log.Println(repo.FindOne(2)) // 這是李四
log.Println(repo.FindOne(3)) // user not found
log.Println(repo.FindOne(4)) //沒有設定4的返回值,卻執行了呼叫,測試不通過
}
// 動態設定返回值
func TestReturnDynamic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 常用方法之一:DoAndReturn(),動態設定返回值
repo.EXPECT().FindOne(gomock.Any()).DoAndReturn(func(i int) (*User,error) {
if i == 0 {
return nil, errors.New("user not found")
}
if i < 100 {
return &User{
Name:"小於100",
}, nil
} else {
return &User{
Name:"大於等於100",
}, nil
}
})
log.Println(repo.FindOne(120))
//log.Println(repo.FindOne(66))
//log.Println(repo.FindOne(0))
}
複製程式碼
呼叫次數檢測
func TestTimes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 預設期望呼叫一次
repo.EXPECT().FindOne(1).Return(&User{Name: "張三"}, nil)
// 期望呼叫2次
repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil).Times(2)
// 呼叫多少次可以,包括0次
repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found")).AnyTimes()
// 驗證一下結果
log.Println(repo.FindOne(1)) // 這是張三
log.Println(repo.FindOne(2)) // 這是李四
log.Println(repo.FindOne(2)) // FindOne(2) 需呼叫兩次,註釋本行程式碼將導致測試不通過
log.Println(repo.FindOne(3)) // user not found, 不限呼叫次數,註釋掉本行也能通過測試
}
複製程式碼
呼叫順序檢測
func TestOrder(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
o1 := repo.EXPECT().FindOne(1).Return(&User{Name: "張三"}, nil)
o2 := repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
o3 := repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
gomock.InOrder(o1, o2, o3) //設定呼叫順序
// 按順序呼叫,驗證一下結果
log.Println(repo.FindOne(1)) // 這是張三
log.Println(repo.FindOne(2)) // 這是李四
log.Println(repo.FindOne(3)) // user not found
// 如果我們調整了呼叫順序,將導致測試不通過:
// log.Println(repo.FindOne(2)) // 這是李四
// log.Println(repo.FindOne(1)) // 這是張三
// log.Println(repo.FindOne(3)) // user not found
}
複製程式碼
上面的示例只展現了gomock
功能的冰山一角,在本篇中不再深入討論,更多用法請參考文件。
更多官方示例:github.com/golang/mock…
如果你完成了上一章的小練習,嘗試動手使用gomock改造一下吧!
總結一下
本篇介紹了表格驅動測試與gomock測試框架。運用表格驅動測試方法不僅能使測試程式碼更精簡易讀,還能提高我們測試用例的編寫能力,無形中提升了單元測試的質量。gomock的功能十分豐富,想掌握各種騷操作還是要細心閱讀一下官方示例,但通常20%的常規功能也足夠覆蓋80%的測試場景了。
表格驅動單元測試和gomock將我們的單元測試效率與質量提升了一個檔次。在下一篇文章中,將介紹testify
斷言庫,繼續優化我們的單元測試。