詳解Go語言單元測(cè)試中如何解決MySQL存儲(chǔ)依賴問題
在編寫單元測(cè)試的過程中,如果被測(cè)試代碼有外部依賴,為了便于測(cè)試,我們就要想辦法來解決這些外部依賴問題。在做 Web 開發(fā)時(shí),MySQL 存儲(chǔ)就是一個(gè)非常常見的外部依賴,本文就來探討在 Go 語言中編寫單元測(cè)試時(shí),如何解決 MySQL 存儲(chǔ)依賴。
HTTP 服務(wù)程序示例
假設(shè)我們有一個(gè) HTTP 服務(wù)程序?qū)ν馓峁┓?wù),代碼如下:
main.go
package main import ( "encoding/json" "fmt" "gorm.io/gorm" "io" "net/http" "strconv" "github.com/julienschmidt/httprouter" "github.com/jianghushinian/blog-go-example/test/mysql/store" ) func NewUserHandler(db *gorm.DB) *UserHandler { return &UserHandler{ store: store.NewUserStore(db), } } type UserHandler struct { store store.UserStore } func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ... } func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ... } func setupRouter(handler *UserHandler) *httprouter.Router { router := httprouter.New() router.POST("/users", handler.CreateUser) router.GET("/users/:id", handler.GetUser) return router } func main() { mysqlDB, _ := store.NewMySQLDB("localhost", "3306", "user", "password", "test") handler := NewUserHandler(mysqlDB) router := setupRouter(handler) _ = http.ListenAndServe(":8000", router) }
這個(gè)服務(wù)監(jiān)聽 8000
端口,分別提供了兩個(gè) HTTP 接口:
POST /users
用來創(chuàng)建用戶。
GET /users/:id
用來獲取指定 ID 對(duì)應(yīng)的用戶信息。
UserHandler
是一個(gè)結(jié)構(gòu)體,它依賴外部存儲(chǔ)接口 store.UserStore
,這個(gè)接口定義如下:
store/store.go
package store import "gorm.io/gorm" type UserStore interface { Create(user *User) error Get(id int) (*User, error) } func NewUserStore(db *gorm.DB) UserStore { return &userStore{db} } type userStore struct { db *gorm.DB } func (s *userStore) Create(user *User) error { return s.db.Create(user).Error } func (s *userStore) Get(id int) (*User, error) { var user User err := s.db.First(&user, id).Error return &user, err }
store.UserStore
定義了兩個(gè)方法,分別用來創(chuàng)建、獲取用戶信息。
User
模型定義如下:
store/model.go
type User struct { ID int `gorm:"id"` Name string `gorm:"name"` }
store.userStore
結(jié)構(gòu)體則實(shí)現(xiàn)了 store.UserStore
接口。
store.userStore
結(jié)構(gòu)體又依賴了 GORM 庫(kù)的 *gorm.DB
類型,表示一個(gè)數(shù)據(jù)庫(kù)連接對(duì)象。
我們可以使用 NewMySQLDB
建立數(shù)據(jù)庫(kù)連接得到 *gorm.DB
對(duì)象:
store/mysql.go
func NewMySQLDB(host, port, user, pass, dbname string) (*gorm.DB, error) { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, dbname) return gorm.Open(mysql.Open(dsn), &gorm.Config{}) }
至此,這個(gè) HTTP 服務(wù)程序整體邏輯就基本介紹完了。
其目錄結(jié)構(gòu)如下:
$ tree
.
├── go.mod
├── go.sum
├── main.go
└── store
├── model.go
├── mysql.go
└── store.go
為了保證業(yè)務(wù)的正確性,我們應(yīng)該對(duì) (*UserHandler).CreateUser
和 (*UserHandler).GetUser
這兩個(gè) Handler 進(jìn)行單元測(cè)試。
這兩個(gè) Handler 定義如下:
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { w.Header().Set("Content-Type", "application/json") body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()) return } defer func() { _ = r.Body.Close() }() u := store.User{} if err := json.Unmarshal(body, &u); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()) return } if err := h.store.Create(&u); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()) return } w.WriteHeader(http.StatusCreated) } func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { id := ps[0].Value uid, _ := strconv.Atoi(id) w.Header().Set("Content-Type", "application/json") u, err := h.store.Get(uid) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()) return } _, _ = fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, u.ID, u.Name) }
不過,由于文章篇幅所限,我這里僅以測(cè)試 (*UserHandler).GetUser
方法為例,演示如何在測(cè)試過程中解決 MySQL 依賴問題,對(duì) (*UserHandler).CreateUser
方法的測(cè)試就當(dāng)做作業(yè)留給你自己來完成了(當(dāng)然,你也可以到我的 GitHub 上查看我的實(shí)現(xiàn))。
Fake 測(cè)試
我們要為 (*UserHandler).GetUser
方法編寫單元測(cè)試,首先就要分析下這個(gè)方法的外部依賴。
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { id := ps[0].Value uid, _ := strconv.Atoi(id) w.Header().Set("Content-Type", "application/json") u, err := h.store.Get(uid) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintf(w, `{"msg":"%s"}`, err.Error()) return } _, _ = fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, u.ID, u.Name) }
UserHandler
結(jié)構(gòu)本身依賴了 store.UserStore
,這是一個(gè)接口,定義了創(chuàng)建和獲取用戶信息的兩個(gè)方法。
我們使用實(shí)現(xiàn)了 store.UserStore
接口的 store.userStore
結(jié)構(gòu)體來初始化 UserHandler
:
func NewUserHandler(db *gorm.DB) *UserHandler { return &UserHandler{ store: store.NewUserStore(db), } } func NewUserStore(db *gorm.DB) UserStore { return &userStore{db} }
store.userStore
結(jié)構(gòu)體會(huì)使用 GORM 來完成對(duì) MySQL 數(shù)據(jù)庫(kù)的操作。所以,我們分析出 GetUser
方法的第一個(gè)外部依賴實(shí)際上就是 MySQL 存儲(chǔ)。
GetUser
方法還接收三個(gè)參數(shù),它們都屬于 HTTP 網(wǎng)絡(luò)相關(guān)的外部依賴,你可以在我的另一篇文章《在 Go 語言單元測(cè)試中如何解決 HTTP 網(wǎng)絡(luò)依賴問題》中找到解決方案,就不在本文中進(jìn)行講解了。
所以,我們現(xiàn)在重點(diǎn)要關(guān)注的就只有一個(gè)問題,如何解決 MySQL 存儲(chǔ)依賴。
我們來整理下 MySQL 外部依賴的程序調(diào)用鏈:
可以發(fā)現(xiàn),store.UserStore
接口是 UserHandler
和 store.userStore
結(jié)構(gòu)體建立連接的橋梁,我們可以將它作為突破口,實(shí)現(xiàn)一個(gè) Fake object,來替換 store.userStore
結(jié)構(gòu)體。
所謂 Fake object,其實(shí)就是我們同樣要定義一個(gè)結(jié)構(gòu)體,并實(shí)現(xiàn) Create
和 Get
兩個(gè)方法,以此來實(shí)現(xiàn) store.UserStore
接口。
type fakeUserStore struct{} func (f *fakeUserStore) Create(user *store.User) error { return nil } func (f *fakeUserStore) Get(id int) (*store.User, error) { return &store.User{ID: id, Name: "test"}, nil }
與 store.userStore
結(jié)構(gòu)體不同,fakeUserStore
并不依賴 *gorm.DB
,也就不涉及 MySQL 數(shù)據(jù)庫(kù)操作了,這樣就解決了 MySQL 外部存儲(chǔ)依賴。
(*fakeUserStore).Create
方法沒做任何操作,直接返回 nil
,(*fakeUserStore).Get
方法則根據(jù)傳進(jìn)來的 id
返回固定的 User
信息。這也是 Fake object 的特點(diǎn),為真實(shí)對(duì)象實(shí)現(xiàn)一個(gè)簡(jiǎn)化版本。
這樣,我們?cè)诰帉憸y(cè)試代碼時(shí),只需要取代 store.userStore
結(jié)構(gòu)體,使用 fakeUserStore
來實(shí)例化 UserHandler
,就可以避免與 MySQL 數(shù)據(jù)庫(kù)打交道了。
handler := &UserHandler{store: &fakeUserStore{}}
為 (*UserHandler).GetUser
方法編寫的單元測(cè)試完整代碼如下:
func TestUserHandler_GetUser_by_fake(t *testing.T) { handler := &UserHandler{store: &fakeUserStore{}} router := setupRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/users/1", nil) router.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, "application/json", w.Header().Get("Content-Type")) assert.Equal(t, `{"id":1,"name":"test"}`, w.Body.String()) }
現(xiàn)在被測(cè)試的 (*UserHandler).GetUser
方法中通過 h.store.Get(uid)
從數(shù)據(jù)庫(kù)中獲取用戶信息時(shí),就不用再去查詢 MySQL 了,而是由 (*fakeUserStore).Get
方法直接返回 Fake 數(shù)據(jù)。
使用 go test
來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="TestUserHandler_GetUser_by_fake" === RUN TestUserHandler_GetUser_by_fake --- PASS: TestUserHandler_GetUser_by_fake (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/mysql 0.465s
測(cè)試通過。
可以發(fā)現(xiàn),使用 Fake 測(cè)試來解決 MySQL 外部依賴還是比較簡(jiǎn)單的,我們僅需要參考 store.userStore
實(shí)現(xiàn)一個(gè)簡(jiǎn)化版本的 fakeUserStore
,然后在測(cè)試過程中,使用簡(jiǎn)化版本的 fakeUserStore
對(duì)象替換掉 store.userStore
即可。
Mock 測(cè)試
前文中,我們使用 fakeUserStore
來替換 store.userStore
,以此來接口 MySQL 依賴問題。
不過,這種使用 Fake object 來解決外部依賴的方式存在兩個(gè)較為常見的弊端:
一個(gè)是使用 Fake object 需要手動(dòng)編寫大量代碼,這里的 store.UserStore
接口僅定義了兩個(gè)方法還好,但一個(gè)線上的復(fù)雜業(yè)務(wù),可能有幾十個(gè)接口,每個(gè)接口又有幾十個(gè)方法,此時(shí)如果還是手動(dòng)來編寫這些代碼,需要消耗大量時(shí)間。
另一個(gè)是 Fake object 返回結(jié)果比較固定,如果想測(cè)試其他情況,比如查詢的 User
不存在,需要報(bào)錯(cuò)的情況,就得在 (*fakeUserStore).Get
方法中編寫更多的邏輯,這增加了實(shí)現(xiàn) Fake object 的復(fù)雜度。
那么有沒有一種替代方案,來彌補(bǔ) Fake object 的這兩個(gè)弊端呢?
答案是使用 Mock 測(cè)試。
Mock 和 Fake 類似,本質(zhì)上都是使用一個(gè)對(duì)象,去替代另一個(gè)對(duì)象。Fake 測(cè)試是實(shí)現(xiàn)了一個(gè)真實(shí)對(duì)象(store.userStore
)的簡(jiǎn)化版本(fakeUserStore
),Mock 測(cè)試則是使用模擬對(duì)象來斷言真實(shí)對(duì)象被調(diào)用時(shí)的輸入符合預(yù)期,然后通過模擬對(duì)象返回指定輸出。
在 Go 中,我們可以使用 gomock 來實(shí)現(xiàn) Mock 測(cè)試。
gomock
項(xiàng)目起源于 Google 的 golang/mock 倉(cāng)庫(kù)。不幸的是,谷歌不再維護(hù)這個(gè)項(xiàng)目了。幸運(yùn)的是,這個(gè)項(xiàng)目由 Uber fork 了一份,并繼續(xù)維護(hù)。
gomock
包含兩個(gè)部分:gomock
包和 mockgen
命令行工具。gomock
包用來完成對(duì)被 Mock 對(duì)象的生命周期管理,mockgen
工具則用來自動(dòng)生成 Mock 代碼。
可以通過如下方式來安裝 gomock
包和 mockgen
工具:
$ go get go.uber.org/mock/gomock@latest $ go install go.uber.org/mock/mockgen@latest
注意:在項(xiàng)目根目錄下通過 go get
命令獲取 gomock
包后,不要急著執(zhí)行 go mod tidy
,因?yàn)楝F(xiàn)在 gomock
包屬于 indirect
依賴,還沒有被使用。當(dāng)通過 mockgen
工具生成了 Mock 代碼以后,再來執(zhí)行 go mod tidy
,go.mod
文件中才不會(huì)丟失 gomock
依賴。
要想使用 gomock
來模擬 store.UserStore
接口的實(shí)現(xiàn),我們先要使用 mockgen
工具來生成 Mock 代碼:
$ mockgen -source store/store.go -destination store/mocks/gomock.go -package mocks
-source
參數(shù)指明需要 Mock 的接口文件路徑,即 store.UserStore
接口所在文件。
-destination
參數(shù)指明生成的 Mock 文件路徑。
-package
參數(shù)指明生成的 Mock 文件包名。
在項(xiàng)目根目錄下執(zhí)行 mockgen
命令,即可生成 Mock 文件:
// Code generated by MockGen. DO NOT EDIT. // Source: store/store.go // Package mocks is a generated GoMock package. package mocks import ( reflect "reflect" store "github.com/jianghushinian/blog-go-example/test/mysql/store" gomock "go.uber.org/mock/gomock" ) // MockUserStore is a mock of UserStore interface. type MockUserStore struct { ctrl *gomock.Controller recorder *MockUserStoreMockRecorder } // MockUserStoreMockRecorder is the mock recorder for MockUserStore. type MockUserStoreMockRecorder struct { mock *MockUserStore } // NewMockUserStore creates a new mock instance. func NewMockUserStore(ctrl *gomock.Controller) *MockUserStore { mock := &MockUserStore{ctrl: ctrl} mock.recorder = &MockUserStoreMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockUserStore) EXPECT() *MockUserStoreMockRecorder { return m.recorder } // Create mocks base method. func (m *MockUserStore) Create(user *store.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", user) ret0, _ := ret[0].(error) return ret0 } // Create indicates an expected call of Create. func (mr *MockUserStoreMockRecorder) Create(user interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserStore)(nil).Create), user) } // Get mocks base method. func (m *MockUserStore) Get(id int) (*store.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", id) ret0, _ := ret[0].(*store.User) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. func (mr *MockUserStoreMockRecorder) Get(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUserStore)(nil).Get), id) }
提示:生成的 mocks 包代碼你無需全部看懂,僅知道它大概生成了什么內(nèi)容,如何使用即可。
可以發(fā)現(xiàn),mockgen
為我們生成了 mocks.MockUserStore
結(jié)構(gòu)體,并且實(shí)現(xiàn)了 Create
、Get
兩個(gè)方法,即實(shí)現(xiàn)了 store.UserStore
接口。
現(xiàn)在,我們就可以使用生成的 Mock 對(duì)象來編寫單元測(cè)試代碼了:
func TestUserHandler_GetUser_by_mock(t *testing.T) { ctrl := gomock.NewController(t) // 斷言 mockUserStore.Get 方法會(huì)被調(diào)用 defer ctrl.Finish() mockUserStore := mocks.NewMockUserStore(ctrl) mockUserStore.EXPECT().Get(2).Return(&store.User{ ID: 2, Name: "user2", }, nil) handler := &UserHandler{store: mockUserStore} router := setupRouter(handler) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/users/2", nil) router.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, "application/json", w.Header().Get("Content-Type")) assert.Equal(t, `{"id":2,"name":"user2"}`, w.Body.String()) }
gomock.NewController(t)
用來創(chuàng)建一個(gè) Mock 控制器,該對(duì)象可以控制整個(gè) Mock 生命周期。
ctrl.Finish()
用來斷言 Mock 對(duì)象使用 EXPECT()
方法設(shè)置的期待執(zhí)行方法會(huì)被調(diào)用,一般使用 defer
語句來調(diào)用,防止最后忘記。不過,如果你使用的 Go 版本大于 1.14,則可以不必顯式調(diào)用 ctrl.Finish()
。
mocks.NewMockUserStore(ctrl)
使用 Mock 控制器創(chuàng)建了 *mocks.MockUserStore
對(duì)象,有了它,我們就可以模擬調(diào)用 store.UserStore
接口對(duì)應(yīng)方法的邏輯了:
mockUserStore.EXPECT().Get(2).Return(&store.User{ ID: 2, Name: "user2", }, nil)
mockUserStore
對(duì)象就相當(dāng)于我們前文中實(shí)現(xiàn)的 fakeUserStore
。
Mock 對(duì)象的 EXPECT()
方法用來設(shè)置預(yù)期被調(diào)用的方法,以及被調(diào)用方法所期望的輸入,它支持鏈?zhǔn)秸{(diào)用,.Get(2)
表示期望在測(cè)試中調(diào)用 Mock 對(duì)象 mockUserStore
的 Get
方法時(shí),輸入?yún)?shù)是 2
,Return
方法用來設(shè)置輸出,即返回值內(nèi)容。
這就相當(dāng)于,我們實(shí)現(xiàn)了 fakeUserStore
的 Get
方法。
我們可以使用 mockUserStore
來實(shí)例化 UserHandler
對(duì)象。
在 req
請(qǐng)求中,我們?cè)O(shè)置請(qǐng)求的用戶 ID 值為 2
,即 mockUserStore
對(duì)象斷言中的參數(shù),二者參數(shù)匹配,Mock 對(duì)象才能生效。
單元測(cè)試最后,斷言了返回結(jié)果為 {"id":2,"name":"user2"}
,即 mockUserStore
對(duì)象期望的返回結(jié)果。
現(xiàn)在我們就可以測(cè)試 (*UserHandler).GetUser
方法了。
使用 go test
來執(zhí)行測(cè)試函數(shù):
$ go test -v -run="TestUserHandler_GetUser_by_mock" === RUN TestUserHandler_GetUser_by_mock --- PASS: TestUserHandler_GetUser_by_mock (0.00s) PASS ok github.com/jianghushinian/blog-go-example/test/mysql 0.220s
測(cè)試通過。
使用 Mock 測(cè)試來解決 MySQL 外部依賴問題,我們無需手動(dòng)編寫 Mock 對(duì)象的代碼,可以使用 mockgen
工具為我們自動(dòng)生成,簡(jiǎn)化了 Fake 測(cè)試中編寫 fakeUserStore
的過程。
并且,如果想要測(cè)試其他情況,僅需要再次使用 Mock 對(duì)象的 EXPECT()
方法來設(shè)置 Get
方法的期望輸入和輸出即可。
比如設(shè)置預(yù)期查詢 ID 為 3
的用戶信息時(shí),返回 user not found
錯(cuò)誤:
mockUserStore.EXPECT().Get(3).Return(nil, errors.New("user not found"))
Mock 測(cè)試更方便我們測(cè)試不同業(yè)務(wù)場(chǎng)景。
gomock 更多用法
gomock
還有一些使用技巧值得分享。
mockgen
前文中,我們使用 mockgen
通過指定源碼文件形式生成了 Mock 代碼:
$ mockgen -source store/store.go -destination store/mocks/gomock.go -package mocks
mockgen
工具還支持通過反射模式來生成 Mock 代碼:
$ mockgen -package mocks -destination store/mocks/gomock.go github.com/jianghushinian/blog-go-example/test/mysql/store UserStore
命令最后的兩個(gè)參數(shù)分別代表需要生成 Mock 代碼的包的導(dǎo)入路徑和逗號(hào)分隔的接口列表。
執(zhí)行以上命令同樣能夠成功生成 Mock 代碼。
此外,我們還可以將 mockgen
命令寫到 Go 文件中,然后使用 Go generate
工具來生成 Mock 代碼:
store/generate.go
package store //go:generate mockgen -package mocks -destination ./mocks/gomock.go . UserStore
這次我們的 mockgen
命令又有所不同,包的導(dǎo)入路徑僅為一個(gè) .
,表示當(dāng)前目錄,這也是被支持的。
這時(shí)候,我們只需要在項(xiàng)目根目錄下執(zhí)行 go generate ./...
命令即可生成 Mock 代碼。./...
表示查找項(xiàng)目下全部文件,go generate
會(huì)自動(dòng)找到帶有 //go:generate
注釋的命令并執(zhí)行。
如果我們有多個(gè)源碼文件要生成 Mock 代碼,go generate
方式就非常合適,僅需要在 Go 文件中分多行依次寫出 mockgen
命令即可使用一條命令一次全部生成。
gomock
前文中,我們使用了 Mock 對(duì)象 mockUserStore
的 EXPECT()
方法來設(shè)置 Get
方法所期待的輸入和輸出。
mockUserStore.EXPECT().Get(2).Return(&store.User{ ID: 2, Name: "user2", }, nil)
有時(shí)候,EXPECT()
所作用的方法可能存在多個(gè)參數(shù),且有些參數(shù)不容易模擬,比如最常見的 context.Context
參數(shù),針對(duì)這些情況,gomock
提供了更多的參數(shù)匹配方法:
gomock.Any()
表示匹配任意參數(shù),適合參數(shù)模擬困難的情況。
gomock.Eq(x)
表示匹配與 x
相等的參數(shù)。
gomock.Not(x)
表示匹配與 x
不想等的參數(shù)。
gomock.Nil()
表示匹配 nil
參數(shù)。
gomock.Len(i)
表示匹配長(zhǎng)度為 i
的參數(shù)。
gomock.All(ms)
表示傳入的所有參數(shù)都想等才能匹配。
以上這些參數(shù)匹配方法都可以像如下這樣使用:
mockUserStore.EXPECT().Get(gomock.Eq(2)).Return(&store.User{ ID: 2, Name: "user2", }, nil)
此外,我們可以約束 EXPECT()
所作用方法的執(zhí)行次數(shù):
.Return(xxx).Times(2) // 預(yù)期方法會(huì)被調(diào)用 2 次 .Return(xxx).MaxTimes(2) // 預(yù)期方法最多執(zhí)行 2 次 .Return(xxx).MinTimes(2) // 預(yù)期方法至少執(zhí)行 2 次 .Return(xxx).AnyTimes() // 預(yù)期方法執(zhí)行任意次都能匹配
還可以約束 EXPECT()
所作用方法的執(zhí)行順序:
.Return(xxx).After(preReq) // 當(dāng)前預(yù)期方法在 preReq 預(yù)期方法執(zhí)行完成之后執(zhí)行
以上便是我認(rèn)為 gomock
中比較常用的功能講解,更多功能可參考官方文檔。
總結(jié)
本文向大家介紹了在 Go 中編寫單元測(cè)試時(shí),如何解決 MySQL 外部依賴的問題。
我們分別使用了 Fake object 和 Mock 兩種方式,來替換原有的外部依賴。
Web 服務(wù)的代碼不是隨意設(shè)計(jì)的,有意將 UserHandler
依賴的類型設(shè)為 store.UserStore
接口,而不是 store.userStore
結(jié)構(gòu)體,是為了解耦。通過使用接口,解決了 UserHandler
與 store.userStore
結(jié)構(gòu)體強(qiáng)綁定的問題,這就給我們使用 fakeUserStore
或 mockUserStore
來替代 store.userStore
創(chuàng)造了機(jī)會(huì)。
可以發(fā)現(xiàn),本文介紹的兩種方法其實(shí)不僅能夠用于解決 MySQL 外部依賴問題。任何使用接口編寫的代碼,在測(cè)試時(shí)都可以使用這兩種方式來替換依賴。這就是 Go 面向接口編程的好處。
以上就是詳解Go語言單元測(cè)試中如何解決MySQL存儲(chǔ)依賴問題的詳細(xì)內(nèi)容,更多關(guān)于Go單元測(cè)試解決MySQL存儲(chǔ)依賴的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang Http 驗(yàn)證碼示例實(shí)現(xiàn)
這篇文章主要介紹了Golang Http 驗(yàn)證碼示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08gtoken替換jwt實(shí)現(xiàn)sso登錄的排雷避坑
這篇文章主要為大家介紹了gtoken替換jwt實(shí)現(xiàn)sso登錄的排雷避坑,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法
這篇文章主要介紹了GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01