
笔者最近和应该是抄了单测老家,各种mock、打桩场景都遇到很多,近期就遇到了一个需要httpmock的场景,主要是在一个环境中有几个依赖服务应为一些原因没法提供,但是有需要再这个环境上来验证一些其他的服务组件是否正常。这种情况一般有几种解决方式,最典型的就是测试做个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"))
}
特征
它是如何模拟的