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

水立方發表於2019-05-25

通過閱讀上一篇文章,相信你對怎麼做單元測試已經有了初步的概念,可以著手對現有的專案進行改造並開展測試了。學會了走路,我們嘗試跑起來,本篇主要介紹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”。

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

相關文章