我们在开发项目的过程中总会遇到要调用依赖方接口的情况,如果依赖方的API接口还没有开发好,通常我们会先约定好API接口的请求参数、响应结构和各类错误对应的响应码,再按照约定好请求和响应进行开发。
除了上面说的情况外,还有一种就是当你开发的功能需要与微信支付类的API进行对接时,因为各种订单、签名、证书等的限制你在开发阶段也不能直接去调用支付的API来验证自己开发的程序是否能成功完成对接,这种时候我们该怎么办呢?很多人会说发到测试环节让QA造单子测,很多公司里的项目也确实是这么干的。
针对上面说的两种情况,我们有没有什么办法在开发阶段就能通过单元测试来验证我们写的程序符不符合预期呢?这就需要我们掌握对API调用进行Mock的技巧了。
gock 是 Go 生态下一个提供无侵入 HTTP Mock 的工具,用来在单元测试中Mock API 的调用,即不对要请求的API发起真正的调用,而是由gock拦截到请求后返回我们指定的Mock响应。
它是如何模拟的
用 http.DefaultTransport或自定义http.Transport拦截的任何 HTTP 请求流量
将传出的 HTTP 请求与按 FIFO 声明顺序定义的 HTTP 模拟期望池匹配。
如果至少有一个模拟匹配,它将被用来组成模拟 HTTP 响应。
如果没有匹配到的mock,则解析请求报错,除非启用了真实网络模式,在这种情况下,将执行真实的HTTP请求。
gock 的安装方法如下
go get -u github.com/h2non/gock
gock 在官方的Github中给出了一些使用例子
官方GitHub:https://github.com/h2non/gock
官方给出的例子:https://github.com/h2non/gock/tree/master/_examples
这里我找一些典型常用的案例分享给大家,也说一下我在使用后对它们的理解,让大家能更容易上手。
匹配请求头,对匹配到的请求进行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 mocksst.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 mocksst.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 mocksst.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来验证自己开发的程序是否能成功完成对接。
我在《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() }