<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: maxhu</title>
    <description>The latest articles on DEV Community by maxhu (@jhaoheng).</description>
    <link>https://dev.to/jhaoheng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F551226%2F30ad767d-208b-4332-b8b9-5bd02a9eeda3.png</url>
      <title>DEV Community: maxhu</title>
      <link>https://dev.to/jhaoheng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jhaoheng"/>
    <language>en</language>
    <item>
      <title>[go] 使用 mockgen 測試 api</title>
      <dc:creator>maxhu</dc:creator>
      <pubDate>Mon, 10 Jul 2023 07:37:11 +0000</pubDate>
      <link>https://dev.to/jhaoheng/go-shi-yong-mockgen-ce-shi-api-53h3</link>
      <guid>https://dev.to/jhaoheng/go-shi-yong-mockgen-ce-shi-api-53h3</guid>
      <description>&lt;h2&gt;
  
  
  目的
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/golang/mock"&gt;github: mockgen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;mockgen 的做法是使用 cli 的方式，參考已建立好的 interface 建立 mock code。&lt;br&gt;
在更早之前，我都是用手動的方式，mock interface，建立所有的物件，替換掉既有的 interface，做到 interface segregation，但其實這種方式對於維運很累。&lt;br&gt;
所以就出現了 mockgen 神器，透過指令的方式 mock interface，讓測試更容易進行。&lt;br&gt;
下面以 client 端呼叫 server api 作為測試對象，使用 mock http client 以及 mockgen 來作為測試方式。 &lt;/p&gt;


&lt;h2&gt;
  
  
  API Test 的 api 測試方式
&lt;/h2&gt;

&lt;p&gt;使用 mock http client 的方式，用於替換掉 http.Client 中的 Transport interface，塞入一個我們想要回傳的內容。&lt;br&gt;
避免 http.NewRequest() 直接去呼叫正式的環境，讓測試邏輯可以在本地端進行驗證。&lt;br&gt;
client 只需要驗證從 api 取得的物件格式，解析後的物件在接下來的邏輯中運作正常。&lt;/p&gt;

&lt;p&gt;如下建立一個 api 物件, 包含 interface, struct 與 method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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 &amp;amp;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 &amp;amp;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
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;再建立 RoundTripper 物件，用來 mock http.Client 的 transport interface&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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 &amp;amp;http.Client{
        Transport: newTranspost,
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;下面就是我們的測試，使用 SetClient() 替換掉既有的 http.Client&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func TestApiByHttptest(t *testing.T) {
    var client = NewTestHttpClient(
        func(req *http.Request) *http.Response {
            return &amp;amp;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  使用 mockgen 的 api 測試方式
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;使用指令建立 mock 檔案, &lt;code&gt;mockgen -source=api.go -destination api_mock.go -package api&lt;/code&gt;, 會產生 api_mock.go 檔案&lt;/li&gt;
&lt;li&gt;單元測試
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  mockgen 使用 context 傳遞 interface 物件
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;在 func 中使用 api 並執行測試
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;測試
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  結論
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;mockgen 在單元測試下，可以很輕鬆地做到測試&lt;/li&gt;
&lt;li&gt;mockgen 在複雜的結構下，可使用 ctx，將物件放入 func 其中進行測試&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>mockgen</category>
      <category>test</category>
      <category>api</category>
    </item>
  </channel>
</rss>
