
测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载gock 是一个零依赖、直接构建在 Go 标准库net/http之上的 HTTP 模拟mocking库通过实现http.RoundTripper接口透明地拦截全部出站 HTTP 请求并按照声明顺序匹配预先定义的 Mock 期望来构造响应。本文以仓库 vendor 目录下的 gock README 为主体结合其全部源码实现gock.go、transport.go、request.go、response.go 等以及本仓库 pkg/riskanalysis/cmd_test.go 中的真实用例完整讲解 gock 的安装、拦截原理、声明式 API、匹配规则、网络切换与自定义扩展帮助你在单元测试与运行时场景中彻底摆脱真实外部服务的依赖。gock 是什么gock 是一款多才多艺Versatile的 Go HTTP Mock 库它的设计目标是让 HTTP 模拟变得简单并且能配合任何基于net/http标准库实现的客户端工作。它深受 Node.js 生态中著名的 nock 启发nock 本身还有 Python 移植版 pook因此在 API 风格上保留了声明式、语义化、链式调用的特征。在本仓库中gock 以 vendor 依赖的形式存在于vendor/github.com/h2non/gock/并被实际用于pkg/riskanalysis/cmd_test.go的测试代码中用来拦截风险分析命令对 HTTP 后端的请求——这正是 gock 的典型落地场景在测试中模拟外部 HTTP 服务使测试不依赖真实网络。核心特性从 README 可以归纳出 gock 的能力清单简单、富有表现力、流畅fluent的 API语义化的 API DSL用于声明式地定义 HTTP Mock内置 JSON/XML 模拟辅助函数支持持久化persistentMock 与带 TTL 计数限制volatile的 Mock支持完整正则表达式能力的 HTTP 请求匹配同时面向测试场景与运行时场景设计可按 HTTP 方法、URL 参数、请求头与请求体进行匹配可扩展、可插拔的 HTTP 匹配规则可在 Mock 模式与真实网络模式之间切换可对请求进行过滤Filter与映射Map以实现精确匹配内置 map 与 filter 机制便于管理多个 Mock基于http.RoundTripper接口实现兼容面广兼容任何net/http客户端如 gentleman 等第三方客户端支持网络超时/取消延迟delay模拟API 可扩展、可定制零依赖Dependency free。安装与引入gock 的安装方式与绝大多数 Go 库一致直接使用go getgo get -u github.com/h2non/gock本仓库将其固定为 vendor 依赖源码位于vendor/github.com/h2non/gock/当前版本号可从 version.go 中确认。项目代码中只需通过常规 import 引入即可import github.com/h2non/gock详细的 API 文档以 GoDoc 为准README 中给出 godoc 引用入口下文将结合源码逐一展开讲解。拦截原理gock 是如何完成 Mock 的README 用四个步骤概括了 gock 的拦截工作流通过http.DefaultTransport或任意http.Client使用的自定义http.Transport拦截所有出站 HTTP 请求将出站请求与一组已定义的 HTTP Mock 期望按FIFO先进先出声明顺序逐一匹配只要有一个 Mock 匹配成功就用它来组合出模拟的 HTTP 响应如果没有任何 Mock 匹配则返回错误除非开启了真实网络模式real networking此时会执行一次真实的 HTTP 请求。在源码层面这四步分别由以下核心组件实现拦截器Transport结构体实现http.RoundTripper接口transport.goRoundTrip方法transport.go是请求进入 Mock 流程的总入口。全局默认的DefaultTransport NewTransport()并缓存原生NativeTransport http.DefaultTransport以便恢复transport.go。匹配RoundTrip中调用MatchMock(req)matcher.go它按注册顺序遍历 Mock 栈并调用每个 Mock 的Match方法mock.go。组合响应匹配成功后调用Responder(req, mock.Response(), res)responder.go将 Mock 中定义的状态码、Header、Body、延迟等字段写入http.Response。未匹配处理没有 Mock 命中且未开启网络时返回ErrCannotMatch errors.New(gock: cannot match any request)同时把该请求记录到unmatchedRequests注册表transport.go、gock.go。关键的设计细节是Intercept()会把http.DefaultTransport替换为 gock 的DefaultTransportgock.go因此任何默认使用http.DefaultClient的代码都会自动被拦截无需改动业务代码。而Intercepting()通过判断http.DefaultTransport DefaultTransport来报告当前是否处于拦截状态gock.go。shouldUseNetworktransport.go则负责裁决是否走真实网络当 Mock 显式设置UseNetwork、或全局config.Networking开启且网络过滤器全部放行时请求会透传给原始 Transport 执行。声明式 API从 Mock 到响应的完整 DSLgock 的声明式 DSL 从gock.New(uri)开始它负责解析 URL、创建请求与响应对象、注册 Mock 到全局存储并返回请求 DSLgock.go。注意New内部会调用normalizeURI为未显式携带http:///https://前缀的 URI 自动补全http://gock.go。一个最基础的 Mock 声明形如gock.New(http://server.com). Get(/bar). Reply(200). JSON(map[string]string{foo: bar})这一串链式调用完整覆盖了请求 DSL 与响应 DSL 两段下面分别拆解。请求侧 DSLrequest.goRequest结构体request.go承载所有待匹配的请求条件包括Counter剩余可匹配次数、Persisted是否持久化、URLStruct、Method、Header、Cookies、PathParams、BodyBuffer、Mappers、Filters等字段。其方法一览方法作用说明Get/Post/Put/Delete/Patch/Head(path)声明期望的 HTTP 方法与 URL 路径内部通过method()将方法名转为大写并写入URLStruct.Pathrequest.goURL(uri)/SetURL(u)/Path(path)精确指定待匹配的 URL 或路径Body(io.Reader)/BodyString(string)/File(path)以流、字符串或文件内容定义请求体匹配JSON(data)/XML(data)以结构体定义 JSON/XML 请求体并自动设置Content-Type未设置时默认application/json/application/xmlrequest.goMatchType(kind)匹配请求的Content-TypeMIME 头支持别名json、xml、form、text、url等见 matchers.go 的BodyTypeAliasesBasicAuth(user, pass)定义 HTTP Basic 认证按 RFC 2617 对user:pass做 base64 编码后写入Authorization头request.goMatchHeader(key, value)匹配指定请求头值支持正则HeaderPresent(key)要求某请求头存在即可内部实现为把值设为.*request.goMatchHeaders(map)批量匹配多个请求头MatchParam(key, value)/MatchParams(map)匹配 URL 查询参数值支持正则写入URLStruct.RawQueryParamPresent(key)要求某查询参数存在同样以.*实现request.goPathParam(key, val)匹配 RESTful 路径参数例如/users/123/name可用PathParam(users, 123)匹配request.goPersist()将 Mock 设为持久化命中后不删除Times(num)指定 Mock 保持活跃的命中次数写入Counterrequest.goAddMatcher/SetMatcher追加或替换自定义匹配函数委托给 Mock 的 matcherrequest.goMap(fn)/Filter(fn)注册请求映射/过滤函数在匹配前处理请求EnableNetworking()对当前 Mock 单独开启真实网络设置Response.UseNetwork truerequest.goReply(status)定义响应状态码并切换到响应 DSL等价于Response.Status(status)ReplyError(err)模拟一个响应错误写入Response.ErrorReplyFunc(fn)通过自定义函数定制响应接收*Response作为参数request.go响应侧 DSLresponse.goResponse结构体response.go定义拦截后返回的响应内容主要方法方法作用Status(code)设置响应状态码Type(kind)设置响应的Content-Type同样支持别名SetHeader/AddHeader/SetHeaders设置/追加/批量设置响应头Body(io.Reader)/BodyString(string)/File(path)以流、字符串或文件内容定义响应体JSON(data)/XML(data)以结构体生成 JSON/XML 响应体并自动设置 MIME 头SetError(err)注入模拟错误Delay(duration)模拟响应延迟当前仍标记为 experimental 特性Map(fn)/Filter(fn)注册响应映射/过滤函数EnableNetworking()对当前响应开启真实网络值得注意的底层实现Responderresponder.go在组装响应时会依次处理错误注入、响应过滤器、状态码、Header 合并、Body 写入、响应映射器最后按ResponseDelay用time.NewTimer睡眠模拟延迟并通过select监听req.Context().Done()支持请求取消若请求上下文已结束会关闭响应体并返回上下文错误。这意味着gock 的延迟与超时模拟是感知 context 取消的不会在测试中造成无谓的挂起。生命周期与计数器默认每个 Mock 的Counter初始为 1request.go即命中一次后即失效调用Times(n)可调整为 n 次。命中时Mocker.decrement()会递减计数器减到 0 时通过disabler.Disable()将 Mock 标记为禁用mock.goPersist()则跳过递减逻辑。Mocker.Done()判断 Mock 是否已用完mock.go全局存储的Clean()会定期移除已完成的 Mockstore.go。gock.IsDone()/gock.IsPending()用于断言是否还有未命中的 Mockstore.go这是测试收尾时验证所有 Mock 都被消费的利器。实战示例从简单 Mock 到完整测试README 提供了从入门到进阶的完整示例这里全部保留并补充源码级注解。在测试中做简单 Mock这是最经典的用法先声明 Mock再执行测试逻辑最后用defer gock.Off()清空。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 dont have pending mocks st.Expect(t, gock.IsDone(), true) }注意这里http.Get走的是默认客户端无需任何额外配置即被 gock 拦截——这正是Intercept()替换http.DefaultTransport的功劳。gock.IsDone()返回 true 说明所有注册的 Mock 都已被命中消费。请求头匹配MatchHeader的值支持正则表达式HeaderPresent只要求字段存在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) st.Expect(t, gock.IsDone(), true) }源码实现上MatchHeaders会对每个声明的头部字段用regexp.MatchString匹配请求中的对应值如果值包含()等正则保留字符匹配失败还会退化为regexp.QuoteMeta转义后的字面匹配matchers.go兼顾了正则能力与字面语义。URL 查询参数匹配MatchParam同样支持正则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?page1per_page10, 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) st.Expect(t, gock.IsDone(), true) }对应的匹配实现MatchQueryParams会对每个声明的查询参数在请求 URL 中逐一正则匹配matchers.go。JSON 请求体匹配与响应MatchType(json)声明请求的 MIME 类型JSON(...)既可作为请求体匹配条件也可作为响应体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}) st.Expect(t, gock.IsDone(), true) }请求体匹配的逻辑相当智能matchers.go依次尝试原子字符串比较、正则匹配最后如果双方都能json.Unmarshal成 map还会用reflect.DeepEqual做结构级 JSON 相等比较——这意味着即使 JSON 字段顺序不同也能正确匹配。匹配完成后会通过createReadCloser恢复req.Body流避免影响后续读取。若请求体的Content-Encoding为gzip还会用gzip.NewReader自动解压后再比较matchers.go。拦截自定义 http.Client 与 http.RoundTripper如果被测代码使用自定义http.Client而非默认客户端需要用gock.InterceptClient(client)显式接管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) st.Expect(t, gock.IsDone(), true) }InterceptClient的实现非常巧妙它把客户端原来的 Transport 保存到新建的 gockTransport内部再替换为 gock 的 Transportgock.go如果客户端已被拦截过则直接跳过。相应地RestoreClient会把原始 Transport 归还给客户端gock.go。开启真实网络混合模式gock 允许 Mock 与真实网络共存命中的 Mock 使用模拟响应未命中的请求透传到真实服务package main import ( fmt io/ioutil net/http github.com/h2non/gock ) func main() { defer gock.Off() defer gock.DisableNetworking() gock.EnableNetworking() gock.New(http://httpbin.org). Get(/get). Reply(201). SetHeader(Server, gock) res, err : http.Get(http://httpbin.org/get) if err ! nil { fmt.Errorf(Error: %s, err) } // The response status comes from the mock fmt.Printf(Status: %d\n, res.StatusCode) // The server header comes from mock as well fmt.Printf(Server header: %s\n, res.Header.Get(Server)) // Response body is the original body, _ : ioutil.ReadAll(res.Body) fmt.Printf(Body: %s, string(body)) }这个例子的结果很有趣由于httpbin.org/get命中了 Mock状态码201与 Server 头来自 Mock而响应体是真实服务的原始返回。这是因为shouldUseNetwork判定走真实网络后Transport.RoundTrip先执行真实请求拿到res再由Responder用 Mock 的状态码与 Header 覆盖真实响应transport.go。EnableNetworking/DisableNetworking是全局开关gock.goNetworkingFilter还能按请求粒度决定哪些请求放行到真实网络。调试观察被拦截的 HTTP 请求gock.Observe注册一个观察函数可在请求经过拦截器时看到请求与匹配结果gock.DumpRequest是内置实现会把 HTTP/1.x 线协议格式的请求完整打印出来package main import ( bytes net/http github.com/h2non/gock ) func main() { defer gock.Off() gock.Observe(gock.DumpRequest) gock.New(http://foo.com). Post(/bar). MatchType(json). JSON(map[string]string{foo: bar}). Reply(200) body : bytes.NewBuffer([]byte({foo:bar})) http.Post(http://foo.com/bar, application/json, body) }DumpRequest通过httputil.DumpRequestOut打印请求原始字节并输出Matches: true/false指示是否命中 Mockgock.go。观察回调在RoundTrip中于匹配之后、响应组装之前被调用transport.go。使用技巧与最佳实践README 给出了一组经过实战检验的 Tips本仓库的测试代码也严格遵循了这些约定。先声明 Mock再写测试逻辑在测试函数开头声明完所有 Mock 之后再进入被测代码见上文TestFoo示例并在函数入口用defer gock.Off()兜底清理。Off()的实现是Flush()清空全部已注册 Mock加Disable()恢复http.DefaultTransportgock.go。警惕并发下的竞态条件gock并非完全线程安全但关键部分如全局存储、计数器、disabler均受互斥锁保护。如果测试涉及并发代码应确保 Mock 在并发发起前就已声明完毕避免在配置 gock 或拦截自定义客户端时产生意外竞态。从源码看全局 config 的读写均有mutex保护gock.goMock 计数器使用sync.Mutexdisabler 使用sync.RWMutexmock.goMock 存储使用sync.RWMutexstore.goTransport 自身也有互斥锁transport.go。先定义复杂 Mock再定义通用 Mock同一测试套件中若有多个 Mock建议先声明更具体约束更多的 Mock再声明通用 Mock。因为匹配按 FIFO 顺序进行先声明具体的 Mock 能避免请求被宽松但先到的通用 Mock如只匹配路径不匹配 Header/Body抢先命中。用完后关闭拦截为把测试代码的副作用降到最低测试逻辑结束后应关闭 gock 的流量拦截。惯用做法就是deferfunc TestGock (t *testing.T) { defer gock.Off() // ... my test code goes here }对同一个 http.Client 只拦截一次同一个http.Client实例无需重复拦截在测试场景开头调用一次gock.InterceptClient(client)即可——源码中InterceptClient检测到 Transport 已是 gock 类型时会直接返回gock.go。拦截后恢复 http.Client注意使用http.DefaultClient或http.DefaultTransport时无需恢复因为gock.Off()已还原默认 Transport。但如果拦截的是自定义客户端建议在测试场景结束后调用gock.RestoreClient(client)恢复原始 Transport可用defer与gock.Off()配合func TestGock (t *testing.T) { defer gock.Off() defer gock.RestoreClient(client) // ... my test code goes here }可扩展机制自定义匹配器与请求预处理gock 的匹配层是完全可插拔的。Matcher接口matcher.go定义了Get/Add/Set/Flush/Match默认实现MockMatcher内置一组匹配函数MatchersHeaderMatchMethod、MatchScheme、MatchHost、MatchPath、MatchHeaders、MatchQueryParams、MatchPathParamsmatcher.goMatchersBodyMatchBodymatcher.goDefaultMatcher NewMatcher()全量启用并可通过NewBasicMatcher()仅头部匹配或NewEmptyMatcher()无内置匹配定制matcher.go。MockMatcher.Match按序执行所有匹配函数任一失败即整体失败matcher.go。自定义规则有两种入口gock.New(...).AddMatcher(fn)为单个 Mock 追加一个MatchFuncfunc(*http.Request, *Request) (bool, error)类型的匹配函数gock.New(...).SetMatcher(matcher)整体替换该 Mock 的匹配器实现。此外Request.Map/Request.Filter可以在正式匹配之前改写或筛选请求request.goMocker.Match内部会先执行 Filters不满足直接不匹配再执行 Mappers把改写后的请求用于匹配mock.go。README 中给出了两个进阶示例入口_examples/add_matchers新增匹配函数与_examples/custom_matcher自定义匹配层。在本仓库中的真实应用gock 并非停留在第三方介绍层面——本仓库的pkg/riskanalysis/cmd_test.go就是它的直接用户。该测试文件在风险分析命令的测试中通过defer gock.Off()保证测试结束后清理 Mock 与拦截cmd_test.go用gock.New(url).Reply(500)模拟 HTTP 500 错误响应用gock.New(url).Reply(200).BodyString(reqContent)模拟带特定响应体的 200 响应cmd_test.go通过gock.InterceptClient(client)接管测试代码使用的自定义http.Clientcmd_test.go在另一组用例中用gock.New(url).Persist().Reply(500)声明持久化 Mock保证多次请求都能命中cmd_test.go。这完整演示了 README 中测试场景 自定义客户端 持久化 Mock的组合拳可作为在大型仓库中引入 gock 的参照范例。已知限制与注意事项线程安全README 明确说明 gock 不是完全线程安全的sensible parts 是安全的并发场景下应先声明 Mock 再发起请求。Host 正则默认MatchHost会把gock.New中传入的 Host 当正则表达式使用若 Host 含正则保留字符需要按字面匹配可用Options{DisableRegexpHost: true}关闭options.go、matchers.go。Body 匹配的 MIME 白名单MatchBody只支持BodyTypes中列出的文本类 MIME如text/html、application/json、application/xml、multipart/form-data、application/x-www-form-urlencodedmatchers.go。压缩仅支持 gzipCompressionSchemes目前只有gzipmatchers.go。延迟模拟为实验特性Response.Delay在源码注释中被标记为 experimental未来可能改进response.go。未匹配请求的追踪GetUnmatchedRequests()/HasUnmatchedRequest()可检查未被任何 Mock 命中的请求CleanUnmatchedRequest()可清空该注册表gock.go。总结gock 用一个约 1500 行的精简实现把 HTTP Mock 的声明、匹配、拦截、响应组装与网络切换全部封装在http.RoundTripper之上默认客户端零侵入拦截、自定义客户端一行接管、正则级请求匹配、JSON 结构级请求体比较、持久化/次数限制生命周期、真实网络混合模式再加上可插拔的匹配器与请求映射/过滤机制使其既能胜任单元测试中的外部依赖隔离也能在运行时作为流量模拟的轻量方案。无论你是在为 OpenShift 这类大型项目编写测试如本仓库 pkg/riskanalysis/cmd_test.go 的做法还是在独立服务中需要可控的 HTTP 依赖gock 都是一个值得放进工具箱的零依赖选择。赞分享测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载相关推荐【亲测免费】 Gock强大的Go语言HTTP请求模拟库Gock强大的Go语言HTTP请求模拟库 项目介绍 Gock是专为Go语言设计的一个高度灵活且易于使用的HTTP请求模拟库。它允许开发者在进行Web服务测试时go-retryablehttp 使用指南基于 net/http 的自动重试与指数退避 HTTP 客户端go retryablehttp 使用指南基于 net/http 的自动重试与指数退避 HTTP 客户端 go retryablehttp 是 HashiCo构建工具云原生后端undici MockAgent 实战指南用拦截器与 Mock 响应为 Node.js HTTP 代码编写可靠测试undici MockAgent 实战指南用拦截器与 Mock 响应为 Node.js HTTP 代码编写可靠测试 MockAgent 是 undici 提供后端网络通信上一篇Mathias Bynens dotfiles的安全增强配置隐私和安全设置终极指南下一篇OpenSpec自动化测试确保规范与代码一致性的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考