Gock 源码解析

Gock 是 Go 中用来模拟 HTTP 请求,用来 mock net/http 发出请求的工具。

在了解 gock 的工作原理之前,先看下 net/http 是如何工作的。

net/http 的工作原理

http 有两种用法,一种是用 DefaultTransport 发出请求,一种是用用户创建的 Transport 发出请求。

// 第一种用法
res, err := http.Get("http://foo.com/bar")

// 第二种用法
req, err := http.NewRequest("GET", "http://foo.com", nil)
client := &http.Client{Transport: &http.Transport{}}
res, err := client.Do(req)

http.Get 时发生了什么

  1. 使用默认 DefaultClient 调用 Get 方法,再调用 Do 方法,最后发出 send 请求
  2. 调用 send 函数时把 DefaultTranport 作为参数传入
  3. send 函数调用 RoundTripper.RoundTrip 方法,再调用 Transport.roundTrip 方法
  4. roundTrip 调用 js 发出 HTTP 请求

http.Client.Do 时发生了什么

  1. 使用自定义的 http.Trasport 生成 http.Client 对象
  2. 剩下的处理和上面流程完全一致

Gock 的工作原理

官方解释

  1. 拦截通过 http.Client 使用的 http.DefaultTransport 或自定义 http.Transport 发出的 HTTP 请求。
  2. 按照先进先出的声明顺序,将传出的 HTTP 请求与已定义的 HTTP 模拟期望库进行匹配。
  3. 如果至少有一个模拟匹配,就会使用它来编写模拟 HTTP 响应。
  4. 如果无法匹配任何模拟,则会以错误方式解析请求,除非启用了真实网络模式,否则将执行真实 HTTP 请求。

第一个简单例子

package test

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

  "github.com/nbio/st"
  "github.com/h2non/gock"
)

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

  gock.New("http://foo.com").
    Get("/bar").
    Reply(200).
    JSON(map[string]string{"foo": "bar"})

  res, err := http.Get("http://foo.com/bar")
  st.Expect(t, err, nil)
  st.Expect(t, res.StatusCode, 200)

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

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

gock.New 做了什么

  1. 对 http.DefaultTransport 进行重新赋值,赋值为 gock 的 DefaultTransport
  2. DefaultTransport 是 gock 生成的 Transport 对象,该对象封装了 http.Transport 的结构
  3. gock.New 创建一个 mock 对象,把 request、response 挂载到 mock 对象,最后把 mock 添加到 mocks 队列

gock.New 后再调用 http.Get 发生了什么

  1. 因为 gock.New 时重新对 http.DefaultTransport 进行了赋值
  2. http.Get 调用 Transport.RoundTrip 时,实际调用的 mock 自己的 Transport.RoundTrip 函数
  3. MatchMock 遍历 mocks,寻找匹配的 mock
  4. Match 到 mock 后,对 mock.request.Counter 减一,如果 Counter 等于 0,则设置 mock.disabler.disabled 状态为 true
  5. 通过 shouldUseNetwork 判断是否走网络
    • 如果不走网络,把 req 记录到 unmatchedRequests 队列
    • 如果走网络,调用 m.Transport.RoundTrip 走真实的 http/net

gock.Off 的作用

清空 mocks 队列,恢复 http.DefaultTransport

gock.IsDone 的作用

判断若 mocks 中所有 mock 都如预期被调用(所有的 mock.disabler.disable 的状态都为 true),则返回 true。

第二个例子

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

唯一区别是调用了 gock.InterceptClient(client),目的是对 client.Transport 重新赋值。剩下处理流程完全一致。

// gock 源码
func InterceptClient(cli *http.Client) {
	_, ok := cli.Transport.(*Transport)
	if ok {
		return // if transport already intercepted, just ignore it
	}
	trans := NewTransport()
	trans.Transport = cli.Transport
	cli.Transport = trans
}

参考

  • https://github.com/h2non/gock