一、前言
笔者最近和应该是抄了单测老家,各种mock、打桩场景都遇到很多,近期就遇到了一个需要httpmock的场景,主要是在一个环境中有几个依赖服务应为一些原因没法提供,但是有需要再这个环境上来验证一些其他的服务组件是否正常。这种情况一般有几种解决方式,最典型的就是测试做个mock服务,但由于诸多问题这个任务落到了研发这边,我们就在思考怎么来做这个事情,大致想到了一下几种方案:
- 单独写个http-service提供mock能力,修改服务发现地址
- 在程序启动时启动一个http端口提供mock服务,在改写服务请求地址
- 在逻辑层进行判断读取相关返回参数
- 还有一个就是我提出的通过 gock 无侵入来mock
首先我们排除了方案3,太过于侵入逻辑层,并且也不够灵活对于各种延迟网络请求失败场景无法模拟,第二个我们排除了方案一,要再去管理一个mock的服务开一套git仓库也有些得不偿失,最终我们在方案二和方案四之前进行选择,最终gock胜出,应为它够工具化,启动、加载、热更新、事件触发等等都可以通过一个配置文件来实现,自己洗一套http服务来实现模块还要设计各类的场景和配置。然后就有了今天的软件分享,来和大家一起聊聊gock这个无侵入的http mock 工具吧。
PS1:除了上述场景对于本地开发依赖三方的http接口,比如发送邮件发送短信联调需要判断异常流程,编写单测用例对三方一来场景进行模拟等都非常的适合
PS2:gock的实现原理需要基于 net/http 包或以 net/http 包作为底层的 http-client,比如底层完成独立的fasthttp就没法实现mock的效果
参考资料
二、如何使用
我们可以看见下面这个examples,对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)
}
更多的例子:
请求头匹配
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)
}
添加匹配器函数
package main
import (
"fmt"
"net/http"
"gopkg.in/h2non/gock.v1"
)
func main() {
defer gock.Off()
gock.New("http://httpbin.org").
Get("/").
AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { return req.URL.Scheme == "http", nil }).
AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) { return req.Method == ereq.Method, nil }).
Reply(204).
SetHeader("Server", "gock")
res, err := http.Get("http://httpbin.org/get")
if err != nil {
fmt.Errorf("Error: %s", err)
}
fmt.Printf("Status: %d\n", res.StatusCode)
fmt.Printf("Server header: %s\n", res.Header.Get("Server"))
}
自定义匹配层
package main
import (
"fmt"
"gopkg.in/h2non/gock.v1"
"net/http"
)
func main() {
defer gock.Off()
// Create a new custom matcher with HTTP headers only matchers
matcher := gock.NewBasicMatcher()
// Add a custom match function
matcher.Add(func(req *http.Request, ereq *gock.Request) (bool, error) {
return req.URL.Scheme == "http", nil
})
// Define the mock
gock.New("http://httpbin.org").
SetMatcher(matcher).
Get("/").
Reply(204).
SetHeader("Server", "gock")
res, err := http.Get("http://httpbin.org/get")
if err != nil {
fmt.Errorf("Error: %s", err)
}
fmt.Printf("Status: %d\n", res.StatusCode)
fmt.Printf("Server header: %s\n", res.Header.Get("Server"))
}
三、它有哪些功能&原理是什么?
特征
- 简单、富有表现力、流畅的API。
- 用于声明性HTTP模拟声明的语义API DSL。
- 内置帮助程序,便于JSON/XML模拟。
- 支持持久性和易失性TTL限制模拟。
- 支持完全正则表达式的HTTP请求模拟匹配。
- 设计用于测试和运行时场景。
- 按方法、URL参数、标头和正文匹配请求。
- 可扩展和可插入的HTTP匹配规则。
- 能够在模拟和真实网络模式之间切换。
- 能够过滤/映射HTTP请求以实现精确的模拟匹配。
- 支持映射和过滤器以轻松处理模拟。
- 使用HTTP的广泛兼容HTTP侦听器。往返器接口。
- 适用于任何与net/http兼容的客户端,例如Gentler。
- 网络超时/取消延迟模拟。
- 可扩展且可黑客攻击的API。
- 无依赖性。
它是如何模拟的
- http.DefaultTransport或自定义http.Transport拦截的任何 HTTP 请求流量
- 将传出的 HTTP 请求与按 FIFO 声明顺序定义的 HTTP 模拟期望池匹配。
- 如果至少有一个模拟匹配,它将被用来组成模拟 HTTP 响应。
- 如果没有匹配到的mock,则解析请求报错,除非启用了真实网络模式,在这种情况下,将执行真实的HTTP请求。