【gock】 无侵入的 Http Mock 工具

在这里插入图片描述

一、前言

笔者最近和应该是抄了单测老家,各种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请求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文振熙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值