[go] 使用 mockgen 測試 api
Tags: go, mockgen, test, api
Published: 2023-07-10 07:37:11

---
title: [go] 使用 mockgen 測試 api
published: true
description: 
tags: #golang #mockgen #test #api
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-10 07:31 +0000
---

## 目的

- [github: mockgen](https://github.com/golang/mock)

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 其中進行測試