Go Mock -gomock- 簡明教程

1 gomock 簡介

上一篇文章 Go Test 單元測試簡明教程 介紹了 Go 語言中單元測試的常用方法,包括子測試 (subtests)、表格驅動測試(table-driven tests)、幫助函數(helpers)、網絡測試和基準測試(Benchmark) 等。這篇文章介紹一種新的測試方法,mock/stub 測試,當待測試的函數 / 對象的依賴關係很複雜,並且有些依賴不能直接創建,例如數據庫連接、文件 I/O 等。這種場景就非常適合使用 mock/stub 測試。簡單來說,就是用 mock 對象模擬依賴項的行爲。

GoMock is a mocking framework for the Go programming language. It integrates well with Go’s built-in testing package, but can be used in other contexts too.

gomock 是官方提供的 mock 框架,同時還提供了 mockgen 工具用來輔助生成測試代碼。

使用如下命令即可安裝:

go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen

2 一個簡單的 Demo

type DB interface {
	Get(key string) (int, error)
}

func GetFromDB(db DB, key string) int {
	if value, err := db.Get(key); err == nil {
		return value
	}

	return -1
}

假設 DB 是代碼中負責與數據庫交互的部分 (在這裏用 map 模擬),測試用例中不能創建真實的數據庫連接。這個時候,如果我們需要測試 GetFromDB 這個函數內部的邏輯,就需要 mock 接口 DB

第一步:使用 mockgen 生成 db_mock.go。一般傳遞三個參數。包含需要被 mock 的接口得到源文件source,生成的目標文件destination,包名package

$ mockgen -source=db.go -destination=db_mock.go -package=main

第二步:新建 db_test.go,寫測試用例。

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish() 

	m := NewMockDB(ctrl)
	m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist"))

	if v := GetFromDB(m, "Tom"); v != -1 {
		t.Fatal("expected -1, but got", v)
	}
}

最終的代碼結構如下:

project/
    |--db.go
    |--db_mock.go // generated by mockgen
    |--db_test.go

執行測試:

$ go test . -cover -v
=== RUN   TestGetFromDB
--- PASS: TestGetFromDB (0.00s)
PASS
coverage: 81.2% of statements
ok      example 0.008s  coverage: 81.2% of statements

3 打樁 (stubs)

在上面的例子中,當 Get() 的參數爲 Tom,則返回 error,這稱之爲打樁(stub),有明確的參數和返回值是最簡單打樁方式。除此之外,檢測調用次數、調用順序,動態設置返回值等方式也經常使用。

3.1 參數 (Eq, Any, Not, Nil)

m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
m.EXPECT().Get(gomock.Any()).Return(630, nil)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil) 
m.EXPECT().Get(gomock.Nil()).Return(0, errors.New("nil"))

3.2 返回值 (Return, DoAndReturn)

m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)
m.EXPECT().Get(gomock.Any()).Do(func(key string) {
    t.Log(key)
})
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error) {
    if key == "Sam" {
        return 630, nil
    }
    return 0, errors.New("not exist")
})

3.3 調用次數 (Times)

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	m := NewMockDB(ctrl)
	m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2)
	GetFromDB(m, "ABC")
	GetFromDB(m, "DEF")
}

3.4 調用順序 (InOrder)

func TestGetFromDB(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish() 

	m := NewMockDB(ctrl)
	o1 := m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
	o2 := m.EXPECT().Get(gomock.Eq("Sam")).Return(630, nil)
	gomock.InOrder(o1, o2)
	GetFromDB(m, "Tom")
	GetFromDB(m, "Sam")
}

4 如何編寫可 mock 的代碼

寫可測試的代碼與寫好測試用例是同等重要的,如何寫可 mock 的代碼呢?

在軟件工程中,依賴注入的意思爲,給予調用方它所需要的事物。 “依賴”是指可被方法調用的事物。依賴注入形式下,調用方不再直接指使用 “依賴”,取而代之是“注入” 。“注入” 是指將 “依賴” 傳遞給調用方的過程。在 “注入” 之後,調用方纔會調用該“依賴”。傳遞依賴給調用方,而不是讓讓調用方直接獲得依賴,這個是該設計的根本需求。
依賴注入 - Wikipedia

如果 GetFromDB() 方法長這個樣子

func GetFromDB(key string) int {
	db := NewDB()
	if value, err := db.Get(key); err == nil {
		return value
	}

	return -1
}

DB 接口的 mock 並不能作用於 GetFromDB() 內部,這樣寫是沒辦法進行測試的。那如果將接口 db DB 通過參數傳遞到 GetFromDB(),那麼就可以輕而易舉地傳入 Mock 對象了。



本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://geektutu.com/post/quick-gomock.html