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 时发生了什么
- 使用默认 DefaultClient 调用 Get 方法,再调用 Do 方法,最后发出 send 请求
- 调用 send 函数时把 DefaultTranport 作为参数传入
- send 函数调用 RoundTripper.RoundTrip 方法,再调用 Transport.roundTrip 方法
- roundTrip 调用 js 发出 HTTP 请求
http.Client.Do 时发生了什么
- 使用自定义的 http.Trasport 生成 http.Client 对象
- 剩下的处理和上面流程完全一致
Gock 的工作原理
官方解释
- 拦截通过 http.Client 使用的 http.DefaultTransport 或自定义 http.Transport 发出的 HTTP 请求。
- 按照先进先出的声明顺序,将传出的 HTTP 请求与已定义的 HTTP 模拟期望库进行匹配。
- 如果至少有一个模拟匹配,就会使用它来编写模拟 HTTP 响应。
- 如果无法匹配任何模拟,则会以错误方式解析请求,除非启用了真实网络模式,否则将执行真实 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 做了什么
- 对 http.DefaultTransport 进行重新赋值,赋值为 gock 的 DefaultTransport
- DefaultTransport 是 gock 生成的 Transport 对象,该对象封装了 http.Transport 的结构
- gock.New 创建一个 mock 对象,把 request、response 挂载到 mock 对象,最后把 mock 添加到 mocks 队列
gock.New 后再调用 http.Get 发生了什么
- 因为 gock.New 时重新对 http.DefaultTransport 进行了赋值
- http.Get 调用 Transport.RoundTrip 时,实际调用的 mock 自己的 Transport.RoundTrip 函数
- MatchMock 遍历 mocks,寻找匹配的 mock
- Match 到 mock 后,对 mock.request.Counter 减一,如果 Counter 等于 0,则设置 mock.disabler.disabled 状态为 true
- 通过 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