Skip to content

用gock 拦截HTTP请求,Mock API调用 #92

@kevinyan815

Description

@kevinyan815

我们在开发项目的过程中总会遇到要调用依赖方接口的情况,如果依赖方的API接口还没有开发好,通常我们会先约定好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中给出了一些使用例子

这里我找一些典型常用的案例分享给大家,也说一下我在使用后对它们的理解,让大家能更容易上手。

gock 的使用案例

我们可以看见下面这个基础的使用案例,对https://test.com/api/v1/users 进行了mock,并且通过NetworkingFilter过滤非test.com域名的方式让baidu.com这种还是访问真实流量,并且还能通过Filter来触发mock过程中的time.sleep或者是启动一个goroutine来进行异步逻辑触发等。

package examples

import (
	"fmt"
	"net/http"
	"testing"

	"gopkg.in/h2non/gock.v1"
)

func Test_HTTPMock(t *testing.T) {
	defer gock.Off()
	defer gock.DisableNetworking()
	gock.EnableNetworking() // 开启真实流量模式,否则所有的http请求都会被mock

	// 配置适应自身业务的拦截规则
	gock.NetworkingFilter(func(request *http.Request) bool {
		fmt.Println(request.Host)
		return request.Host != "test.com"
	})

	// 设定mock域名路由和返回信息
	gock.New("https://test.com").Get("/api/v1/users").
		Persist().
		Reply(200).
		JSON(map[string][]string{"mock": {"123456"}})

	// 被mock拦截
	resp, err := http.Get("https://test.com/api/v1/users")
	fmt.Println(resp)
	fmt.Println(err)

	// 正常请求
	resp, err = http.Get("https://baidu.com")
	fmt.Println(resp)
	fmt.Println(err)
}

匹配请求头,对中匹配到的请求进行Mock

package test

import (
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

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)
}

请求参数匹配

package test

import (
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

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 请求体匹配

package test

import (
  "bytes"
  "github.com/nbio/st"
  "gopkg.in/h2non/gock.v1"
  "io/ioutil"
  "net/http"
  "testing"
)

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 拦截到。

// 参考链接: https://github.com/h2non/gock#mocking-a-custom-httpclient-and-httproundtripper
package test

import (
  "io/ioutil"
  "net/http"
  "testing"

  "github.com/nbio/st"
  "github.com/h2non/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测试

我在《Go项目搭建和整洁开发实战》的单元测试实战部分,给跟微信支付API对接的程序做了单元测试出了使用到gock外,还应用了gomonkey mock了程序中到的项目对接层的私有方法

Image

感兴趣的可以订阅我的专栏跟大家一起学习:

或者微信扫下方二维码查看和订阅专栏

Image

参考链接:https://blog.csdn.net/u011142688/article/details/126082691

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions