搞定Go單元測試(二)—— mock框架(gomock)

經過閱讀上一篇文章,相信你對怎麼作單元測試已經有了初步的概念,能夠着手對現有的項目進行改造並開展測試了。學會了走路,咱們嘗試跑起來,本篇主要介紹gomock測試框架,讓咱們的單元測試更加有效率。git

表格驅動測試方法(Table Driven Tests)

當針對某方法進行單元測試的時候,一般不止寫一個測試用例,咱們須要測試該方法在多種入參條件下是否都能正常工做,特別是要針對邊界值進行測試。一般這個時候表格驅動測試就派上用場了——當你發現你在寫測試方法的時候用上了複製粘貼,這就說明你須要考慮使用表格驅動測試來構建你的測試方法了。咱們依舊來舉個例子:github

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)
        }
    }
}
複製代碼

表格驅動測試方法讓咱們的測試方法更加清晰和簡練,減小了複製粘貼,並大大提升的測試代碼的可讀性。golang

還記得上文說單元測試也是須要維護的嗎?單元測試也是代碼的一部分,也應當被認真對待。記得要用表格驅動測試的方法來組織你的測試用例,同時別忘了像正式代碼那樣,寫上相應的註釋。 更多參考: github.com/golang/go/w… blog.golang.org/subtestsbash

使用測試框架——gomock

What is gomock?

gomock是Google開源的golang測試框架。或者引用官方的話來講:「GoMock is a mocking framework for the Go programming language」。框架

github.com/golang/mock函數

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斷言庫,繼續優化咱們的單元測試。

相關文章
相關標籤/搜索