尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TEN-framework 测试体系中的 Catch2 Matchers 完全指南:从内置断言到自定义 Matcher 实战

TEN-framework 测试体系中的 Catch2 Matchers 完全指南:从内置断言到自定义 Matcher 实战 人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载Catch2 的 Matchers匹配器机制是本仓库测试体系的重要基石TEN-framework 的核心运行时C 语言实现与各语言扩展绑定均大量依赖 Catch2 及其REQUIRE_THAT、CHECK_THAT系列宏进行复杂类型与浮点数值断言。本文以仓库内第三方依赖 Catch2随 clingo-sys 一并引入版本为 3.8.0见 catch_version_macros.hpp的官方 Matchers 文档为骨架系统讲解内置 Matcher 的用法、组合规则、生命周期陷阱以及新旧两种自定义 Matcher 的编写范式读者学完后可直接在 TEN-framework 的 C 测试如 tests/ten_runtime/smoke中落地使用。什么是 Matchers断言的一种可组合替代方案Matchers 由 Hamcrest 框架推广是传统断言如REQUIRE(x y)之外的另一条书写路径特别适用于两类场景被测对象是复杂类型如容器、异常、结构体或需要断言某个复合属性如以某后缀结尾且包含某子串。其核心价值在于可组合性单个 Matcher 可通过、||、!自由组合可扩展性用户可以编写自己的 Matcher并与 Catch2 内置 Matcher 无缝混用可读性失败时输出的是 Matcher 的describe()描述而非晦涩的表达式求值结果。在 TEN-framework 中Catch2 通过 third_party/clingo-sys/clingo/third_party/catch 引入其src/catch2/matchers/目录下完整提供了文档所述的全部头文件包括catch_matchers_string.hpp、catch_matchers_vector.hpp、catch_matchers_floating_point.hpp、catch_matchers_exception.hpp、catch_matchers_templated.hpp、catch_matchers_quantifiers.hpp与catch_matchers_range_equals.hpp等。使用 MatchersREQUIRE_THAT 与 CHECK_THATMatcher 最常见的载体是REQUIRE_THAT与CHECK_THAT两个宏。以REQUIRE_THAT为例它接收两个参数第一个是被测输入对象/值第二个是 Matcher 本身。using Catch::Matchers::EndsWith; REQUIRE_THAT( getSomeString(), EndsWith(as a service) );单个 Matcher 还可以用 C 逻辑运算符、||、!组合using Catch::Matchers::EndsWith; using Catch::Matchers::ContainsSubstring; REQUIRE_THAT( getSomeString(), EndsWith(as a service) ContainsSubstring(web scale));上面这个断言要求getSomeString的返回值既以 as a service 结尾又在某处包含 web scale。两个字符串 Matcher 都位于catch_matchers_string.hpp头文件中因此编译上述代码需要#include catch2/matchers/catch_matchers_string.hpp在仓库中该头文件定义了StartsWithMatcher、EndsWithMatcher、StringContainsMatcher、StringEqualsMatcher与RegexMatcher五个具体类它们统一继承自StringMatcherBase见 catch_matchers_string.hpp实际匹配逻辑与描述逻辑都在该基类中完成。组合运算符与生命周期陷阱IMPORTANT组合运算符并不接管 Matcher 对象的所有权。这意味着如果你把组合后的 Matcher 存储起来必须保证被组合的各个 Matcher 比组合 Matcher 活得更久。!产生的否定 Matcher 也属于组合 Matcher同样适用该规则。下面的用法是安全的——无论是临时组合还是先保存单个 Matcher 再组合// 直接临时组合安全 CHECK_THAT(value, WithinAbs(0, 2e-2) !WithinULP(0., 1)); // 先保存单个 Matcher再组合安全 auto is_close_to_zero WithinAbs(0, 2e-2); auto is_zero WithinULP(0., 1); CHECK_THAT(value, is_close_to_zero !is_zero);但下面的写法是未定义行为use-after-freeUAFauto is_close_to_zero WithinAbs(0, 2e-2); auto is_zero WithinULP(0., 1); auto is_close_to_but_not_zero is_close_to_zero !is_zero; // 隐患 CHECK_THAT(a_value, is_close_to_but_not_zero); // UAF原因在于!is_zero会创建一个临时的 Negation Matcher 实例is_close_to_but_not_zero保存了对它的引用当该行语句结束后临时对象被销毁组合 Matcher 便指向了已不存在的对象。从实现看Catch2 内部的组合 Matcher如Detail::MatchAllOf、Detail::MatchAnyOf等见 catch_matchers.hpp保存的是MatcherBaseArgT const*指针而非值这正是不接管所有权设计的根源——因此实际开发中建议优先使用一次性临时组合。内置 Matchers 全景Catch2 的每个 Matcher 都拆分为两部分位于Catch::Matchers命名空间的工厂函数以及位于更深层命名空间、用户不应直接使用的实际 Matcher 类型。例如文档示例中的Catch::Matchers::Contains是工厂函数真正执行匹配的是Catch::Matchers::StdString::ContainsMatcher类型。仓库中这一模式随处可见catch_matchers_string.hpp末尾即同时声明了Equals、ContainsSubstring、EndsWith、StartsWith、Matches五个工厂函数与其返回的具体 Matcher 类型。std::string MatchersCatch2 提供 5 个针对std::string的内置 Matcher签名统一带一个可选的CaseSensitive第二参数默认区分大小写即CaseSensitive::Yes工厂函数匹配语义StartsWith(std::string str, CaseSensitive)参数以str开头EndsWith(std::string str, CaseSensitive)参数以str结尾ContainsSubstring(std::string str, CaseSensitive)参数在某处包含strEquals(std::string str, CaseSensitive)参数与str完全相等唯一条件Matches(std::string str, CaseSensitive)用str作为 ECMAScript 正则对参数进行整体匹配需要特别强调的是Matches的语义匹配针对整个字符串进行因此正则abc无法匹配输入abcd若想匹配后者需要使用abc.*这类能覆盖全串的正则。在 catch_matchers_string.hpp 中可以看到五个工厂函数均提供了caseSensitivity CaseSensitive::Yes的默认实参用户可通过传入CaseSensitive::No关闭大小写敏感。Vector Matchers已弃用见通用 Range MatchersVector Matchers 已被功能相同的通用 Range Matchers 取代并标记为 deprecated但为了兼容旧代码Catch2 仍保留 5 个作用于std::vector的内置 Matcher工厂函数语义Contains检查指定的 vector 是否作为整体出现在结果中VectorContains检查指定元素是否出现在结果中Equals结果与目标 vector 精确相等顺序相关UnorderedEquals结果与目标 vector 在某一排列下相等顺序无关Approx结果与目标 vector 近似相等顺序相关逐元素用Approx比较Catch2 2.7.2 引入示例用法std::vectorint some_vec{ 1, 2, 3 }; REQUIRE_THAT(some_vec, Catch::Matchers::UnorderedEquals(std::vectorint{ 3, 2, 1 }));该断言会通过因为{3, 2, 1}是some_vec中元素的一个排列。这些 Matcher 位于catch2/matchers/catch_matchers_vector.hpp仓库路径 catch_matchers_vector.hpp。浮点数 MatchersCatch2 提供 4 个针对浮点数的 Matcher用于解决 IEEE-754 浮点直接比较的精度问题工厂函数语义引入版本WithinAbs(double target, double margin)与目标值的绝对差不超过 margin早于 2.xWithinULP(FloatingPoint target, uint64_t maxUlpDiff)与目标值的ULP 差不超过 maxUlpDiff早于 2.xWithinRel(FloatingPoint target, FloatingPoint eps)与目标值的相对误差不超过 epsCatch2 2.10.0IsNaN()输入为 NaN 即匹配Catch2 3.3.2前三个用于比较两个浮点数具体原理与推荐场景可参考文档 comparing-floating-point-numbers.md。IsNaN则如其名输入是 NaN 即通过。相比直接写REQUIRE(std::isnan(x))用REQUIRE_THAT(x, IsNaN())的最大优势在于——检查失败时你能在测试输出中看到x的实际值而不是只知道isnan 为假。仓库实现印证在 catch_matchers_floating_point.hpp 中可以看到WithinAbsMatcher保存m_target与m_margin、WithinUlpsMatcher保存m_ulps与 IEEE 754 类型信息、WithinRelMatcher默认 eps 为 100 倍DBL_EPS/FLT_EPS且 float 版本利用了float 提升为 double 无损的 IEEE-754 性质以及IsNaNMatcher的完整声明。杂项 Matchers谓词与异常匹配除分类内置 Matcher 外Catch2 还提供了一些工具类 Matcher。Predicate(Callable pred, std::string description)用任意可调用对象构造 Matcher匹配时对参数调用preddescription用于设置 Matcher 的自我描述。注意必须显式指定参数类型例如REQUIRE_THAT(Hello olleH, Predicatestd::string( [] (std::string const str) - bool { return str.front() str.back(); }, First and last character should be equal) );其实现位于 catch_matchers_predicate.hpp。异常匹配因为异常比较特殊Catch2 提供了独立的宏REQUIRE_THROWS_MATCHES(expr, ExceptionType, Matcher)该宏断言expr抛出的异常派生自ExceptionType随后对捕获到的异常调用Matcher::match。宏本身位于catch2/matchers/catch_matchers.hpp仓库路径 catch_matchers.hpp。对于一次性检查可直接使用PredicateREQUIRE_THROWS_MATCHES(parse(...), parse_error, Predicateparse_error([] (parse_error const err) - bool { return err.line() 1; }) );但若打算系统化测试错误报告官方文档建议为特定异常类型定义专用 Matcher。此外Catch2 为检查异常的错误消息异常需派生自std::exception提供 2 个内置 MatcherMessage(std::string message)要求what()返回的消息与message完全相等MessageMatches(Matcher matcher)将提供的 Matcher 应用到what()返回的消息上可与std::stringMatcher如StartsWith配合使用Catch2 3.3.0 引入。示例REQUIRE_THROWS_MATCHES(throwsDerivedException(), DerivedException, Message(DerivedException::what)); REQUIRE_THROWS_MATCHES(throwsDerivedException(), DerivedException, MessageMatches(StartsWith(DerivedException)));仓库中对应的ExceptionMessageMatcher与ExceptionMessageMatchesMatcher定义在 catch_matchers_exception.hpp。通用 Range Matchers通用 Range Matchers 于 Catch2 3.0.1 引入。Catch2 提供基于新式 Matcher 定义、可处理一切range 类类型的通用 Matcher完整清单如下工厂函数语义引入版本IsEmpty()依据std::empty或 ADL 找到的empty自由函数判断为空3.0.1SizeIs(size_t target_size)/SizeIs(Matcher size_matcher)范围大小精确等于目标值或被给定 Matcher 接受3.0.1Contains(T target_element, Comparator std::equal_to{})/Contains(Matcher element_matcher)范围内任一元素等于目标元素或任一元素被给定 Matcher 接受3.0.1AllMatch(Matcher)/AnyMatch(Matcher)/NoneMatch(Matcher)范围内全部/任一/没有元素被给定 Matcher 接受3.0.1AllTrue()/AnyTrue()/NoneTrue()范围内元素bool或可显式转换为bool的类型全部/任一/没有为真3.1.0RangeEquals(TargetRangeLike, Comparator std::equal_to{})与目标 range 逐元素比较全部相等即通过两 range 类型与元素类型不必相同如std::vectorint可与std::arraychar比较3.3.0UnorderedRangeEquals(TargetRangeLike, Comparator std::equal_to{})与RangeEquals类似但顺序无关1,2,3 匹配 3,2,1但不匹配 1,1,2,33.3.0RangeEquals与UnorderedRangeEquals均默认使用operator逐元素比较也都可以通过可选谓词参数自定义比较方式若要对容器逐元素地应用某个 Matcher则应使用AllMatch。从源码看通用 Range Matchers 的实现分布在两个头文件中catch_matchers_quantifiers.hppAllMatchMatcher/NoneMatchMatcher/AnyMatchMatcher其match成员均为模板化的match(RangeLike)通过遍历 range 对每个元素调用内层 Matcher与 catch_matchers_range_equals.hppRangeEqualsMatcher/UnorderedRangeEqualsMatcher同样继承MatcherGenericBase。编写自定义 Matcher旧式风格旧式 Matcher 风格自 Catch Classic 时代延续至今。要编写一个旧式 Matcher需要创建继承自Catch::Matchers::MatcherBaseArgT的自定义类型其中ArgT是该 Matcher 作用的类型必须覆写两个方法bool match(ArgT const) const决定参数是否被接受与std::string describe() const提供人类可读的自我描述官方建议同时编写工厂函数——对模板化 Matcher 而言这在缺乏 CTAD类模板实参推导时主要用于模板实参推导。以下示例实现一个判断参数是否位于某区间内的IsBetweenMatcherT#include catch2/catch_test_macros.hpp #include catch2/matchers/catch_matchers.hpp // ... template typename T class IsBetweenMatcher : public Catch::Matchers::MatcherBaseT { T m_begin, m_end; public: IsBetweenMatcher(T begin, T end) : m_begin(begin), m_end(end) {} bool match(T const in) const override { return in m_begin in m_end; } std::string describe() const override { std::ostringstream ss; ss is between m_begin and m_end; return ss.str(); } }; template typename T IsBetweenMatcherT IsBetween(T begin, T end) { return { begin, end }; } // ... TEST_CASE(Numbers are within range) { // 为 Matcher 的实参类型推断出 double CHECK_THAT(3., IsBetween(1., 10.)); // 为 Matcher 的实参类型推断出 int CHECK_THAT(100, IsBetween(1, 10)); }当然上面的代码还可以继续打磨例如用static_assert约束T为算术类型或泛化到任意可提供比较函数对象的类型。值得注意任何旧式 Matcher 都可以用新式风格重写但旧式 Matcher 的组合通常编译更快新旧两种风格可以任意混用。基类MatcherBase位于catch2/matchers/catch_matchers.hpp仓库路径 catch_matchers.hpp其中MatcherBaseT继承自MatcherUntypedBase仅要求派生类实现一个纯虚的match(T const) const。编写自定义 Matcher新式风格新式 Matcher 于 Catch2 3.0.1 引入。新式 Matcher 要求自定义类型继承Catch::Matchers::MatcherGenericBase并同样提供bool match(...) const与覆写的std::string describe() const。与旧式不同新式对match成员函数的形参没有任何约束参数可以按值、按可变引用传递match成员函数本身也可以是模板化的。这使你可以编写更复杂的 Matcher例如一个比较两个 range 类对象能响应begin/end的 Matcher#include catch2/catch_test_macros.hpp #include catch2/matchers/catch_matchers_templated.hpp // ... templatetypename Range struct EqualsRangeMatcher : Catch::Matchers::MatcherGenericBase { EqualsRangeMatcher(Range const range): range{ range } {} templatetypename OtherRange bool match(OtherRange const other) const { using std::begin; using std::end; return std::equal(begin(range), end(range), begin(other), end(other)); } std::string describe() const override { return Equals: Catch::rangeToString(range); } private: Range const range; }; templatetypename Range auto EqualsRange(const Range range) - EqualsRangeMatcherRange { return EqualsRangeMatcherRange{range}; } TEST_CASE(Combining templated matchers, [matchers][templated]) { std::arrayint, 3 container{{ 1,2,3 }}; std::arrayint, 3 a{{ 1,2,3 }}; std::vectorint b{ 0,1,2 }; std::listint c{ 4,5,6 }; REQUIRE_THAT(container, EqualsRange(a) || EqualsRange(b) || EqualsRange(c)); }示例中EqualsRangeMatcher持有对Range const的引用match通过using std::begin; using std::end;启用 ADL 以便处理任意 range 类型并用std::equal做逐元素比较。与旧式风格相同任何旧式 Matcher 都可以改写为新式但新式 Matcher 的组合在编译期开销上更大新旧风格同样可以任意混用。基类MatcherGenericBase位于catch2/matchers/catch_matchers_templated.hpp仓库路径 catch_matchers_templated.hpp其拷贝赋值与移动赋值均被delete只保留默认构造、拷贝构造与移动构造。仓库中的AllMatchMatcher、RangeEqualsMatcher等通用 Range Matchers 正是基于该基类构建的官方范例可作为自定义新式 Matcher 的参考模板。小结与选型建议综合以上内容在 TEN-framework 的测试代码中使用 Matchers 时可遵循如下决策路径字符串断言优先使用StartsWith/EndsWith/ContainsSubstring/Equals需要正则整体匹配时用Matches注意其全串匹配语义浮点断言按场景选择WithinAbs绝对误差、WithinRel相对误差默认 100 倍机器 epsilon或WithinULP按 ULP 粒度需要断言 NaN 时用IsNaN容器断言优先使用通用 Range MatchersIsEmpty、SizeIs、Contains、AllMatch、AnyMatch、NoneMatch、AllTrue、RangeEquals、UnorderedRangeEquals旧 Vector Matchers 仅用于兼容旧代码异常断言用REQUIRE_THROWS_MATCHES配合Predicate、Message或MessageMatches自定义 Matcher追求更快的组合编译速度用旧式继承MatcherBaseT需要模板化match、跨类型比较等灵活能力时用新式继承MatcherGenericBase务必警惕生命周期陷阱组合运算符不持有所有权禁止将组合结果长期保存后再使用尽量采用一次性临时组合的写法。所有内置 Matcher 的头文件均可在仓库 third_party/clingo-sys/clingo/third_party/catch/src/catch2/matchers 目录下找到源码与本文所述的语义一一对应可直接对照查阅。赞分享人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载相关推荐PowerInfer 测试体系中的 GoogleTest Matchers 参考指南从基础匹配到自定义 MATCHER 宏PowerInfer 测试体系中的 GoogleTest Matchers 参考指南从基础匹配到自定义 MATCHER 宏 本文是 GoogleTest/Go人工智能大模型推理引擎本地部署Jest Expect API 完全指南从内建 Matcher 到自定义断言扩展Jest Expect API 完全指南从内建 Matcher 到自定义断言扩展 编写测试时你经常需要校验某个值是否满足特定条件—— expect 正是 J测试质量保障代码覆盖率开发工具GoogleTest Matchers 参考手册从通配符到自定义 Matcher 的完整实战指南GoogleTest Matchers 参考手册从通配符到自定义 Matcher 的完整实战指南 本篇文章以 GoogleTest 官方 Matchers R测试上一篇RTranslator 模型部署教程1.2GB 离线翻译模型 5 分钟手动装好下一篇Golem用户认证系统OAuth2与JWT集成方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表