资讯详情

资讯详情

cloudpods 中的 Go 断言基石:深入解析 oglematchers 匹配器库的设计与用法

后端云原生运维虚拟化人工智能AI 应用【免费下载链接】cloudpods开源、云原生的多云管理及混合云融合平台项目地址https://gitcode.com/yunionio/cloudpods点击查看免费下载oglematchers是 cloudpods 仓库vendor/github.com/smartystreets/assertions/internal/oglematchers中随smartystreets/assertions一并 vendored 的 Go 匹配器matcher库它被广泛用于项目的单元测试与 mock 校验场景。本文将以该库的 README 与源码为骨架讲清 Matcher 接口的设计哲学、Equals/LessThan/AnyOf等内置匹配器的实现原理以及如何基于它自定义自己的匹配器——读完即可在 cloudpods 的测试代码中熟练运用这套断言原语。1. oglematchers 是什么为测试与 mock 而生的匹配器集合oglematchers是一个面向 Go 语言、专为**测试框架testing framework与 mock 框架mocking framework**设计的匹配器集合。它的设计灵感来自 C 的 [Google Test] 与 [Google JS Test]并且宣称与二者大部分兼容mostly compatible。在上游生态中它被 [ogletest] 测试框架和 [oglemock] mock 框架直接使用而在 cloudpods 仓库中它的价值主要体现在作为github.com/smartystreets/assertionsgoconvey 断言包的底层依赖为ShouldBeGreaterThan、ShouldContain等断言提供通用的值匹配能力。从 cloudpods 仓库的 vendor 目录看该包一共由 11 个源码文件构成是一个体量精巧、职责单一的库vendor/github.com/smartystreets/assertions/internal/oglematchers/ ├── matcher.go # Matcher 接口与 FatalError 定义 ├── equals.go # Equals 匹配器宽松相等 ├── deep_equals.go # DeepEquals 匹配器深度相等 ├── contains.go # Contains 匹配器包含关系 ├── less_than.go # LessThan 匹配器 ├── greater_than.go # GreaterThan 匹配器 ├── less_or_equal.go # LessOrEqual 匹配器 ├── greater_or_equal.go # GreaterOrEqual 匹配器 ├── any_of.go # AnyOf 组合匹配器逻辑或 ├── not.go # Not 组合匹配器逻辑非 └── transform_description.go # 描述文本转换辅助其中 README 示例里出现的HasSubstr(burrito)、MatchesRegex(t.*o)等字符串匹配器在当前 vendored 副本中并未包含对应的实现文件属于上游完整版中才有的能力——从源码结构可以推断这份副本对内置匹配器做了裁剪只保留了与assertions包强相关的核心集合。这一点在使用时值得注意。2. 核心抽象Matcher 接口与 FatalErroroglematchers的全部能力都建立在唯一一个接口之上。在 matcher.go 中定义type Matcher interface { // Check whether the supplied value belongs to the set defined by the // matcher. Return a non-nil error if and only if it does not. Matches(candidate interface{}) error // Description returns a string describing the property that values matching // this matcher have, as a verb phrase where the subject is the value. Description() string }两个方法的语义设计非常讲究Matches(candidate interface{}) error判断候选值是否属于该 matcher 定义的值集合。返回nil表示匹配成功返回非 nil 错误表示不匹配且错误文本被约定为一个关系从句relative clause用于拼接到失败消息中。例如一个匹配包含子串 taco的 matcher 遇到数值 17 时返回which is not a string最终失败消息呈现为Expected: has substring taco Actual: 17, which is not a string如果错误原因本身在 matcher 描述中已显而易见错误文本可以留空但错误本身非 nil例如Expected: 17 Actual: 19Description() string以以值为主语、动词短语形式描述该 matcher 匹配的属性例如is greater than 17、has substring \taco\。这段描述会被用于生成Expected:一侧的输出。FatalError区分不匹配与无法处理接口文档之外matcher.go 还定义了FatalError类型它表示该 matcher 根本不知道如何处理传入的值类型例如GreaterThan(17)拿到了字符串taco。为什么要单独区分这一种错误因为它的传播行为不同组合匹配器如Not遇到FatalError时不会对其进行取反或吞掉而是原样透传。设想如果GreaterThan(17)对taco只是简单地返回 false那么Not(GreaterThan(17))就会返回 true——这在逻辑上没错但极易掩盖用错了匹配器这类编程失误。通过返回 fatal errorNot()会把它继续向上传播从而让真正的问题暴露出来。这也是该库错误处理上最核心的设计点。3. 内置匹配器全览根据当前仓库源码oglematchers提供以下 8 个公开的匹配器构造函数可按用途分为三类类别构造函数匹配语义实现文件数值/字符串比较Equals(x)与 x 宽松相等按底层类型equals.go数值/字符串比较LessThan(x)值 x整数/浮点/字符串less_than.go数值/字符串比较LessOrEqual(x)值 xless_or_equal.go数值/字符串比较GreaterThan(x)值 xgreater_than.go数值/字符串比较GreaterOrEqual(x)值 xgreater_or_equal.go深度/集合DeepEquals(x)与 x 深度相等类型须完全一致deep_equals.go深度/集合Contains(x)slice/array 中存在元素匹配 xcontains.go组合AnyOf(vals...)逻辑或任一子匹配器命中即可any_of.go组合Not(m)逻辑非反转被包裹匹配器的值集合not.goREADME 给出了这样一组简洁的示例直接体现了一切皆匹配器、可自由组合的用法// Numbers Equals(17.13) LessThan(19) // Strings Equals(taco) // Combining matchers AnyOf(LessThan(17), GreaterThan(19))4. 比较类匹配器LessThan 与它的组合子兄弟们4.1 LessThan跨数值类型的比较less_than.go 规定LessThan(x)的入参x必须是整数、浮点数或字符串之一否则直接panic。比较规则为字符串只与字符串比较按字典序v1.String() v2.String()数值之间跨类型可比较所有整数类型int/int8/.../uintptr与浮点类型float32/float64之间都能比较但数值与字符串之间不定义比较整数比较对有符号 vs 无符号做了边界处理例如isSignedInteger(v1) isUnsignedInteger(v2)时需要先判断v1.Int() 0见 compareIntegers涉及 float32 时按float32精度比较避免虚假的高精度导致的误判。若候选值c既非字符串也非数值则返回 fatal errorwhich is not comparable。4.2 LessOrEqual / GreaterThan / GreaterOrEqual由组合派生的优雅实现更有意思的是后三个比较匹配器并不是独立实现而是由更小的原语组合而来less_or_equal.go 中LessOrEqual(x)被实现为return transformDescription(AnyOf(Equals(x), LessThan(x)), desc)即等于 x 或小于 x再通过transformDescription把对外描述统一为less than or equal to %v。源码注释还特意说明把LessThan(x)放在AnyOf的最后一位这样在失败时能优先采用 LessThan 的错误信息保证失败输出可读。greater_than.go 中GreaterThan(x)被实现为Not(LessOrEqual(x))不小于等于即大于greater_or_equal.go 中GreaterOrEqual(x)被实现为Not(LessThan(x))不小于即大于等于。这种由逻辑非 基础比较 描述转换组合出新匹配器的做法正是该库可组合性的典型体现也大幅减少了重复代码。transformDescription的实现在 transform_description.go它只是包一层新描述、把Matches原样委托给内层匹配器。5. Equals基于底层类型的宽松相等Equals(x)是使用频率最高的匹配器它的语义在 equals.go 的文档注释中阐述得非常清晰两条聪明规则值得重点说明5.1 按底层类型做类型检查Go 语言中两个别名类型type alias之间不能直接用比较type stringAlias1 string type stringAlias2 string a : taco b : stringAlias1(taco) c : stringAlias2(taco) a b // 合法通过 b c // 非法无法编译 Equals(b).Matches(a) // 通过 Equals(c).Matches(b) // 通过Equals基于reflect的底层类型underlying type进行比较因此上述别名类型之间也能匹配成功。5.2 数值按抽象数值比较Equals(17)可以同时匹配int(17)、int16(17)、uint(17)、float32(17)、complex64(17)等所有数值类型。实现上Matches会按期望值的 kind 分派到checkAgainstInt64、checkAgainstUint64、checkAgainstFloat32、checkAgainstFloat64、checkAgainstComplex64、checkAgainstComplex128等函数见 equals.go。这些函数内部对有符号/无符号/浮点/复数的交叉比较做了仔细的边界处理并特别小心浮点精度比较 float32 时统一按 float32 精度保证Equals(float32(0.1))能匹配float32(0.1)复数只有虚部为 0 时才可能与实数相等无符号整数只有不超过math.MaxInt64时才可能与有符号数相等。5.3 两类不受支持的值Equals有两个显式的panic边界equals.gostruct不支持构造时直接panic文档建议这类场景改用DeepEquals或reflect.DeepEqual非 nil 的 sliceGo 的不适用于非 nil slice因此同样panic。此外Equals(nil)会被特殊处理为is nil通过checkForNil检查候选值是否是可判 nil 的类型且为 nilequals.go对 chan/func/map/ptr/slice/unsafe.Pointer 等引用类型则通过比较Pointer()判断是否指向同一对象。6. DeepEquals 与 Contains集合与深度相等的处理6.1 DeepEquals严格的深度相等deep_equals.go 的DeepEquals(x)比Equals严格得多要求候选值与 x 的类型完全一致ct ! xt时返回 fatal error然后委托reflect.DeepEqual判定。它还有两个贴心特判byte slice 优化当类型恰好是[]byte且两边均非 nil 时直接用bytes.Equal快速比较避免反射开销deep_equals.gonil slice 的迷惑性fmt.Sprintf会把 nil slice 打印成[]而reflect.DeepEqual严格区分 nil slice 与空 slice因此描述文本中 nil slice 会被标注为nil slice失败时也会给出which is nil的提示避免误导deep_equals.go。6.2 Contains容器包含关系contains.go 的Contains(x)匹配存在至少一个元素匹配 x 的 slice 或 array候选值必须是 slice 或 array否则返回 fatal errorwhich is not a slice or array若x本身不是 Matcher则等价于Contains(DeepEquals(x))即按深度相等逐元素匹配逐元素调用元素匹配器的Matches一旦命中即返回 nilcontains.go。7. 组合匹配器AnyOf 与 Not7.1 AnyOf带非匹配器值支持的逻辑或any_of.go 的AnyOf(vals ...interface{})是逻辑 OR其求值算法在文档注释中按优先级写得很明确若存在实现Matcher接口的值m且m匹配候选值c返回 true否则若存在不实现Matcher 接口的值v且Equals(v)匹配c返回 true即普通值被当作Equals(v)处理否则若存在返回 fatal error 的m则返回该 fatal error保持错误可传播性否则返回 false。实现上构造时对每个入参用反射判断是否实现 Matcher 接口未实现的统一包装成Equals(v)any_of.go描述文本形如or(less than 17, greater than 19)。7.2 Not取反但不吞 fatal errornot.go 的Not(m)语义是反转被包裹匹配器的值集合内层匹配成功err nil→ 返回失败内层返回 fatal error →原样透传不取反内层返回普通错误 → 返回成功。也就是说Not只对明确匹配/不匹配两类结果取反对无法处理的结果保持中立并向上传播。这与第 2 节所述的 FatalError 设计一脉相承。8. 上层应用assertions 包如何消费 oglematchersoglematchers在 cloudpods 中的直接消费方是smartystreets/assertions包。打开 quantity.go 可以看到典型的调用模式——ShouldBeGreaterThan等数量断言本质上就是构造 matcher → 调用 Matches → 根据错误拼装失败消息func ShouldBeGreaterThan(actual interface{}, expected ...interface{}) string { if fail : need(1, expected); fail ! success { return fail } if matchError : oglematchers.GreaterThan(expected[0]).Matches(actual); matchError ! nil { return fmt.Sprintf(shouldHaveBeenGreater, actual, expected[0]) } return success }ShouldBeGreaterThanOrEqualTo、ShouldBeLessThan、ShouldBeLessThanOrEqualTo的结构完全一致分别委托给GreaterOrEqual、LessThan、LessOrEqual。而ShouldBeBetween系列quantity.go则组合调用两个比较断言来判定开/闭区间。在 collections.go 中集合类断言同样基于oglematchers.Contains实现ShouldContain/ShouldNotContain用Contains(expected[0]).Matches(actual)判断成员关系并通过对错误文本which is not a slice or array的比对来区分不是容器与不含该成员两种失败场景ShouldBeIn/ShouldNotBeIncollections.go则反过来用Contains(actual).Matches(expected)判断候选值是否在集合中。再往上这套断言通过So(actual, ShouldBeGreaterThan, expected)这样的入口暴露给测试代码。在 cloudpods 中可以找到大量基于该类断言的单元测试例如 pkg/monitor/alerting/eval_handler_test.go、pkg/monitor/tsdb/driver/influxdb/query_test.go、pkg/monitor/tsdb/time_range_test.go 等是观察断言原语 → 业务测试完整链条的绝佳入口。9. 安装与文档对于希望独立使用oglematchers的开发者README 提供了标准的 Go 模块安装方式要求 Go 1.0.2 或更新版本实际使用以当前 Go 工具链为准go get -u github.com/smartystreets/assertions/internal/oglematchers需要说明的是在 cloudpods 仓库中该包是vendor 目录下的锁定依赖vendor/github.com/smartystreets/assertions/internal/oglematchers构建时直接使用本仓库锁定的副本不需要也不建议单独go get覆盖。离线查看 API 文档可使用godoc github.com/smartystreets/assertions/internal/oglematchers10. 自定义匹配器实现 Matcher 接口即可README 明确指出You can also add your own simply by implementing theoglematchers.Matcherinterface.自定义匹配器只需实现 Matcher 接口。基于前文的分析一个合格的自定义匹配器需要遵守以下约定实现Matches(candidate interface{}) error返回 nil 表示匹配返回普通 error 表示不匹配错误文本最好写成以 which 开头的动名词从句便于拼装失败消息实现Description() string以动词短语描述匹配属性供Expected:输出使用对无法处理的类型返回NewFatalError(...)matcher.go而不是返回普通失败从而保证Not、AnyOf等组合器能正确传播类型错误而非给出误导性结果对外通常以构造函数形式暴露如HasSubstr、GreaterThan那样内部既可定义私有 matcher 类型也可用transformDescription复用现有匹配器并替换描述文本。总结oglematchers以一个接口 两个约定普通错误 / FatalError定义了极其简洁的匹配器抽象再通过Equals、LessThan等原语与AnyOf、Not、transformDescription等组合子构建出一个可无限扩展的断言工具箱。在 cloudpods 中它经由smartystreets/assertions的ShouldBeGreaterThan、ShouldContain等断言成为大量单元测试可读性背后的坚实底座。理解它的设计既能帮你更精准地编写和调试 cloudpods 中的测试也能为你设计自己的 Go 测试 DSL 提供参考。赞分享后端云原生运维虚拟化人工智能AI 应用【免费下载链接】cloudpods开源、云原生的多云管理及混合云融合平台项目地址https://gitcode.com/yunionio/cloudpods点击查看免费下载相关推荐MailHog 项目中的 oglematchers 匹配器库Go 测试与 Mock 场景下的值匹配技术详解MailHog 项目中的 oglematchers 匹配器库Go 测试与 Mock 场景下的值匹配技术详解 本篇指南以仓库内 vendor/github.co后端开发工具KubeSphere 中的 Go YAML 处理基石深入解析 sigs.k8s.io/yaml 的 JSON 桥接设计与实战用法KubeSphere 中的 Go YAML 处理基石深入解析 sigs.k8s.io/yaml 的 JSON 桥接设计与实战用法 导读 sigs.k8s.i云原生容器编排后端微服务多集群DevOps可观测性AI 技能深入解析 Go bencode 编解码库API 设计、类型映射与 cloudpods 中的 BitTorrent 实践深入解析 Go bencode 编解码库API 设计、类型映射与 cloudpods 中的 BitTorrent 实践 bencode 是 BitTorren后端云原生运维虚拟化人工智能AI 应用上一篇低成本双臂机器人XLeRobot让机器人技术触手可及下一篇智能QQ机器人安装配置终极指南从零开始搭建专属聊天助手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →