gomock 原本是 golang 官方維護的套件,不過因為有一段時間沒有在維護了,在 2023 年 6 月時就改由 uber 團隊接手維護了。

安裝工具

安裝 mockgen 工具,gomock 套件

1
2
go install go.uber.org/mock/mockgen@latest
go get go.uber.org/mock/gomock

如何使用

寫好 interface 宣告

寫個簡單的範例,一個 user struct 以及一個 UserRepository interface

user/user.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package user

type User struct {
	ID   uint
	Name string
}

type UserRepository interface {
	Get(id int) (User, error)
	Save(user User) error
}

使用 mockgen 產生 mock struct

使用 mockgen 指令

  • -source: 指定寫 interface 的檔案
  • -destination: 指定產生檔案的位置
1
mockgen -source=user/user.go -destination=mock/user.go

產生的 mock/user.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Code generated by MockGen. DO NOT EDIT.
// Source: user/user.go
//
// Generated by this command:
//
//	mockgen -source=user/user.go -destination=mock/user.go
//

// Package mock_user is a generated GoMock package.
package mock_user

...

// MockUserRepository is a mock of UserRepository interface.
type MockUserRepository struct {
	ctrl     *gomock.Controller
	recorder *MockUserRepositoryMockRecorder
}
...

在測試程式中使用 mock struct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func TestUserRepository(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	repo := mock_user.NewMockUserRepository(ctrl)

	t.Run("Mock Get(1)", func(t *testing.T) {
		// 預期會傳入 1 到 Get(),不符合預期會 fail
		// 回傳 user.User{ID: 1, Name: "John"}
		repo.EXPECT().Get(1).Return(user.User{ID: 1, Name: "John"}, nil)

		// 使用模擬的 repo 執行 user.RunGet()
		user.RunGet(repo)
	})

	t.Run("Stub Get(1) and Get(2)", func(t *testing.T) {
		// 當傳入 2 到 Get() 時,回傳 user.User{ID: 2, Name: "Tom"}
		repo.EXPECT().Get(2).Return(user.User{ID: 2, Name: "Tom"}, nil).AnyTimes()
		// 當傳入 1 到 Get() 時,回傳 user.User{ID: 1, Name: "John"}
		repo.EXPECT().Get(1).Return(user.User{ID: 1, Name: "John"}, nil).AnyTimes()

		// 使用模擬的 repo 執行 user.RunGet()
		user.RunGet(repo)
	})

    t.Run("Stub Save(any)", func(t *testing.T) {
		// 當傳入任何值到 Save() 時,回傳 nil
		repo.EXPECT().Save(gomock.Any()).DoAndReturn(
			func(user user.User) error {
				if user.Name == "" {
					return errors.New("Name is empty")
				}
				return nil
			},
		).AnyTimes()

		// 使用模擬的 repo 執行 user.RunSave()
		user.RunSave(repo)
	})
}

第一個測試案例,預期 mock repository 的 Get() function 會接到 1 的傳入值,並回傳指定 User,如果我們寫的程式不如預期傳入 1 時,這個測試就會失敗。

第二個測試案例,設定了 mock repository 的 Get() function 在接到 1 跟 2 的傳入值會回傳的結果。這時我們寫的程式傳入 1 或 2 時,這個測試都會成功。

第三個測試案例,設定 mock repository 的 Save() function 會檢查傳入的變數 user 的 Name 欄是否為空字串。

搭配 go generate 來產生 mock struct

除了直接執行 mockgen 指令外,我們還可以搭配 go generate 來產生 mock struct 檔案。

go generate 是在程式中加入 generate 註解,格式如下:

1
//go:generate command arg1 arg2

接著執行 go generate ,就會執行註解的指令來產生檔案

我們來寫 go generate 的指令,這時就要用 mockgen 的 Reflect mode 格式

1
mockgen [options] <import path> <interface 1>[,<interface 2>]
  • options: 放額外的參數,至少會寫上 -destination 指令產生檔案的位置
  • import path: 要寫 go 的 import path,在 go generate 裡我們可以用 . 就好
  • interface: 就是我就想要產生 mock struct 的 interface,有多個時可以用 , 串起來

改寫好的 UserRepository 如下:

1
2
3
4
5
6
7
// UserRepository is an interface for user repository.
//
//go:generate mockgen -destination=../mock/user.go . UserRepository
type UserRepository interface {
	Get(id int) (User, error)
	Save(user User) error
}

接著執行指令:

1
go generate -v ./...

我們就可以看到指令把所有的程式檢查過,並成功產生檔案了。

有了這個方法,不管之後有多少的 mock struct 要產生,我們都只要執行 go generate 就可以了。

Reference