目的
mockgen 的做法是使用 cli 的方式,參考已建立好的 interface 建立 mock code。
在更早之前,我都是用手動的方式,mock interface,建立所有的物件,替換掉既有的 interface,做到 interface segregation,但其實這種方式對於維運很累。
所以就出現了 mockgen 神器,透過指令的方式 mock interface,讓測試更容易進行。
下面以 client 端呼叫 server api 作為測試對象,使用 mock http client 以及 mockgen 來作為測試方式。
API Test 的 api 測試方式
使用 mock http client 的方式,用於替換掉 http.Client 中的 Transport interface,塞入一個我們想要回傳的內容。
避免 http.NewRequest() 直接去呼叫正式的環境,讓測試邏輯可以在本地端進行驗證。
client 只需要驗證從 api 取得的物件格式,解析後的物件在接下來的邏輯中運作正常。
如下建立一個 api 物件, 包含 interface, struct 與 method
var URL = "http://my.api"
type IApi interface {
SetClient(client *http.Client) IApi // for httptest use
Request() ([]byte, error)
}
type Api struct {
Client *http.Client
}
func NewApi() IApi {
return &Api{}
}
// for httptest use
func (api *Api) SetClient(client *http.Client) IApi {
api.Client = client
return api
}
func (api *Api) Request() ([]byte, error) {
client := func() *http.Client {
if api.Client == nil {
return &http.Client{}
}
return api.Client
}()
req, err := http.NewRequest("GET", URL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
return data, err
}
再建立 RoundTripper 物件,用來 mock http.Client 的 transport interface
type RoundTripper func(req *http.Request) *http.Response
func (f RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func NewTestHttpClient(newTranspost RoundTripper) *http.Client {
return &http.Client{
Transport: newTranspost,
}
}
下面就是我們的測試,使用 SetClient() 替換掉既有的 http.Client
func TestApiByHttptest(t *testing.T) {
var client = NewTestHttpClient(
func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString("")),
Header: make(http.Header),
}
},
)
result, err := NewApi().SetClient(client).Request()
assert.Nil(t, err)
assert.Equal(t, []byte(""), result)
}
使用 mockgen 的 api 測試方式
- 使用指令建立 mock 檔案,
mockgen -source=api.go -destination api_mock.go -package api, 會產生 api_mock.go 檔案 - 單元測試
func TestApiByGomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockIApi(ctrl)
m.EXPECT().Request().Return([]byte(""), nil)
//
result, err := m.Request()
assert.Nil(t, err)
assert.Equal(t, []byte(""), result)
}
mockgen 使用 context 傳遞 interface 物件
- 在 func 中使用 api 並執行測試
type CtxKey string
const ApiKey CtxKey = "api"
func DoApi(ctx context.Context, x, y int) (int, error) {
_api := ctx.Value(ApiKey)
if svc, ok := _api.(api.IApi); ok {
api_result, err := svc.Request()
logrus.Infof("my api result is: %v", string(api_result))
if err != nil {
return 0, err
}
} else {
panic("api interface wrong")
}
return x + y, nil
}
- 測試
func Test_doSomething(t *testing.T) {
//
ctx := context.Background()
ctx = context.WithValue(ctx, ApiKey, func() any {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := api.NewMockIApi(ctrl)
m.EXPECT().Request().Return([]byte("hello world"), nil).AnyTimes()
return m
}())
err := DoApi(ctx)
assert.Nil(t, err)
}
結論
- mockgen 在單元測試下,可以很輕鬆地做到測試
- mockgen 在複雜的結構下,可使用 ctx,將物件放入 func 其中進行測試
Top comments (0)