Go項目里的API對接,這樣做Mock測試才舒服
我們在開發項目的過程中總會遇到要調用依賴方接口的情況,如果依賴方的API接口還沒有開發好,通常我們會先約定好API接口的請求參數、響應結構和各類錯誤對應的響應碼,再按照約定好請求和響應進行開發。
除了上面說的情況外,還有一種就是當你開發的功能需要與微信支付類的API進行對接時,因為各種訂單、簽名、證書等的限制你在開發階段也不能直接去調用支付的API來驗證自己開發的程序是否能成功完成對接,這種時候我們該怎么辦呢?很多人會說發到測試環節讓QA造單子測,很多公司里的項目也確實是這么干的。
針對上面說的兩種情況,我們有沒有什么辦法在開發階段就能通過單元測試來驗證我們寫的程序符不符合預期呢?這就需要我們掌握對API調用進行Mock的技巧了。
gock
gock 是 Go 生態下一個提供無侵入 HTTP Mock 的工具,用來在單元測試中Mock API 的調用,即不對要請求的API發起真正的調用,而是由gock攔截到請求后返回我們指定的Mock響應。
它是如何模擬的
- 用 http.DefaultTransport或自定義http.Transport攔截的任何 HTTP 請求流量
- 將傳出的 HTTP 請求與按 FIFO 聲明順序定義的 HTTP 模擬期望池匹配。
- 如果至少有一個模擬匹配,它將被用來組成模擬 HTTP 響應。
- 如果沒有匹配到的mock,則解析請求報錯,除非啟用了真實網絡模式,在這種情況下,將執行真實的HTTP請求。
gock 的安裝方法
gock 的安裝方法如下
go get -u github.com/h2non/gock
gock 在官方的Github中給出了一些使用例子
- 官方GitHub:https://github.com/h2non/gock
- 官方給出的例子:https://github.com/h2non/gock/tree/master/_examples
這里我找一些典型常用的案例分享給大家,也說一下我在使用后對它們的理解,讓大家能更容易上手。
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()
}