Go 項目裏的 API 對接,這樣做 Mock 測試才舒服

我們在開發項目的過程中總會遇到要調用依賴方接口的情況,如果依賴方的 API 接口還沒有開發好,通常我們會先約定好 API 接口的請求參數、響應結構和各類錯誤對應的響應碼,再按照約定好請求和響應進行開發。

除了上面說的情況外,還有一種就是當你開發的功能需要與微信支付類的 API 進行對接時,因爲各種訂單、簽名、證書等的限制你在開發階段也不能直接去調用支付的 API 來驗證自己開發的程序是否能成功完成對接,這種時候我們該怎麼辦呢?很多人會說發到測試環節讓 QA 造單子測,很多公司裏的項目也確實是這麼幹的。

針對上面說的兩種情況,我們有沒有什麼辦法在開發階段就能通過單元測試來驗證我們寫的程序符不符合預期呢?這就需要我們掌握對 API 調用進行 Mock 的技巧了。

gock

gock 是 Go 生態下一個提供無侵入 HTTP Mock 的工具,用來在單元測試中 Mock API 的調用,即不對要請求的 API 發起真正的調用,而是由 gock 攔截到請求後返回我們指定的 Mock 響應。

它是如何模擬的

gock 的安裝方法

gock 的安裝方法如下

go get -u github.com/h2non/gock

gock 在官方的 Github 中給出了一些使用例子

這裏我找一些典型常用的案例分享給大家,也說一下我在使用後對它們的理解,讓大家能更容易上手。

gock 的使用案例

匹配請求頭,對匹配到的請求進行 Mock

func TestMatchHeaders(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    MatchHeader("Authorization""^foo bar$").
    MatchHeader("API""1.[0-9]+").
    HeaderPresent("Accept").
    Reply(200).
    BodyString("foo foo")

  req, err := http.NewRequest("GET""http://foo.com", nil)
  req.Header.Set("Authorization""foo bar")
  req.Header.Set("API""1.0")
  req.Header.Set("Accept""text/plain")

  res, err := (&http.Client{}).Do(req)
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 200)
  body, _ := ioutil.ReadAll(res.Body)
  st.Expect(t, string(body)"foo foo")

  // Verify that we don't have pending mocks
  st.Expect(t, gock.IsDone()true)
}

請求參數匹配,對匹配到的請求進行 Mock

func TestMatchParams(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    MatchParam("page""1").
    MatchParam("per_page""10").
    Reply(200).
    BodyString("foo foo")

  req, err := http.NewRequest("GET""http://foo.com?page=1&per_page=10", nil)

  res, err := (&http.Client{}).Do(req)
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 200)
  body, _ := ioutil.ReadAll(res.Body)
  st.Expect(t, string(body)"foo foo")

  // Verify that we don't have pending mocks
  st.Expect(t, gock.IsDone()true)
}

JSON 請求體匹配

func TestMockSimple(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    Post("/bar").
    MatchType("json").
    JSON(map[string]string{"foo""bar"}).
    Reply(201).
    JSON(map[string]string{"bar""foo"})

  body := bytes.NewBuffer([]byte(`{"foo":"bar"}`))
  res, err := http.Post("http://foo.com/bar""application/json", body)
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 201)

  resBody, _ := ioutil.ReadAll(res.Body)
  st.Expect(t, string(resBody)[:13]`{"bar":"foo"}`)

  // Verify that we don't have pending mocks
  st.Expect(t, gock.IsDone()true)
}

上面 JSON 的請求體要跟調用時發送的請求體完全一致,不然 gock 匹配不到這個請求, 如果匹配不上會報錯:gock: cannot match any request。

上面的這些案例都是用的 Go http 的 default client,通常在項目裏會自己封裝 http util 來簡化和標準化項目的 API 請求調用 ,這時候需要把 http util 裏的 client 替換成經過 gock.InterceptClient(client) 攔截的 Client ,這樣用 http util 發起的 API 請求才能 gock 攔截到。

func TestClient(t *testing.T) {
  defer gock.Off()

  gock.New("http://foo.com").
    Reply(200).
    BodyString("foo foo")

  req, err := http.NewRequest("GET""http://foo.com", nil)
  client := &http.Client{Transport: &http.Transport{}}
  gock.InterceptClient(client)

  res, err := client.Do(req)
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 200)
  body, _ := ioutil.ReadAll(res.Body)
  st.Expect(t, string(body)"foo foo")

  // Verify that we don't have pending mocks
  st.Expect(t, gock.IsDone()true)
}

微信支付 API 對接怎麼 Mock 測試

因爲各種訂單、簽名、證書等的限制你在開發階段不能直接去調用支付的 API 來驗證自己開發的程序是否能成功完成對接。

我在《Go 項目搭建和整潔開發實戰》的單元測試實戰部分,給跟微信支付 API 對接的程序做了單元測試,除了使用到 gock 外,還用 gomonkey mock 了程序中用到的項目對接層的私有方法

func TestWxPayLib_CreateOrderPay(t *testing.T) {
 defer gock.Off()
 ......
 request := library.PrePayParam{
  AppId:       payConfig.AppId,
  MchId:       payConfig.MchId,
  OutTradeNo:  order.OrderNo,
  NotifyUrl:   payConfig.NotifyUrl,
  Amount: ...
  Payer: struct {
   OpenId string `json:"open_id"`
  }{OpenId: openId},
 }

 gock.New("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi").
  Post("").MatchType("json").
  JSON(request).
  Reply(200).
  JSON(map[string]string{"prepay_id""wx26112221580621e9b071c00d9e093b0000"})

 wxPayLib := library.NewWxPayLib(context.TODO(), payConfig)
 var s *library.WxPayLib
 patchesOne := gomonkey.ApplyPrivateMethod(s, "getToken", func(_ *library.WxPayLib, httpMethod string, requestBody string, wxApiUrl string) (string, error) {
  token := fmt.Sprintf("mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"",
   payConfig.MchId, "abcddef", time.Now().Unix(), payConfig.PrivateSerialNo, "")
  return token, nil
 })
   ...

 payInfo, err := wxPayLib.CreateOrderPay(order, openId)
 assert.Nil(t, err)
 assert.Equal(t, "e61463f8efa94090b1f366cccfbbb444", payInfo.NonceStr)
 if payInfo.PaySign == "" || payInfo.Package == "" {
  t.Fail()
 }
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T4g1Jkf8g3_DDDrVg1RV3Q