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

资讯详情

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

深入解读 hamcrest-php:Laravel 项目中的 Hamcrest 匹配器 PHP 移植版

深入解读 hamcrest-php:Laravel 项目中的 Hamcrest 匹配器 PHP 移植版 深入解读 hamcrest-phpLaravel 项目中的 Hamcrest 匹配器 PHP 移植版【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples导读本篇技术指南以 hamcrest-php 官方 README 为骨架结合其在 sql-server-samples 仓库的 Laravel 示例vendor/hamcrest/hamcrest-php/中的完整源码实现系统讲解 Hamcrest 匹配器Matchers在 PHP 中的用法、与原始 Java API 的差异以及assertThat断言机制的底层原理。读完本文你将掌握如何用可读性极强的自然语言式断言编写 PHP 单元测试、如何组合多个匹配器表达复杂校验逻辑、以及如何从源码层面理解匹配失败时的描述信息是如何生成的。一、Hamcrest 与 hamcrest-php从 Java 到 PHP 的官方移植Hamcrest 是一个最初为 Java 编写的匹配matching库其核心思想是用“匹配器对象”代替传统断言中的布尔表达式从而让测试断言像自然语言一样可读、可复用、可组合。随后 Hamcrest 被移植到多种语言而hamcrest-php 是 Hamcrest 的官方 PHP 移植版。根据 hamcrest-php README 的说明hamcrest-php 基本遵循对原始 Java API 的直译literal translation只在少数 PHP 语言限制导致的例外处做了调整。其 composer.json 表明该库通过classmap自动加载整个hamcrest目录并额外加载hamcrest/Hamcrest.php仅要求 PHP 5.3.2是一个零依赖的轻量测试库。在 sql-server-samples 仓库的 Laravel 示例 中hamcrest-php 位于vendor/hamcrest/hamcrest-php/是 Laravel 5.1 项目开发依赖链的组成部分。项目根 composer.json 的require-dev声明了phpunit/phpunit: ~4.0、phpspec/phpspec: ~2.1、mockery/mockery: 0.9.*等测试相关依赖而 PHPUnit、Mockery 等框架正是 Hamcrest 匹配器最常见的消费方。也就是说凡是在这个 Laravel 项目里通过 PHPUnit 编写测试都可以直接使用Hamcrest\Matchers提供的匹配器工厂。二、从一行代码开始assertThat基本用法README 给出了一个最简洁的用法示例Hamcrest_MatcherAssert::assertThat(a, Hamcrest_Matchers::equalToIgnoringCase(A));即断言字符串a与A忽略大小写后相等。这一行代码同时展示了 hamcrest-php 的两个核心入口类Hamcrest_MatcherAssert即Hamcrest\MatcherAssert静态断言入口Hamcrest_Matchers即Hamcrest\Matchers全部匹配器的静态工厂。2.1assertThat的三种调用形态查看 MatcherAssert.php 的源码assertThat()通过func_get_args()接收可变参数根据参数个数分三种情况处理参数个数语义失败行为1 个直接断言布尔表达式为真抛出无消息的AssertionError2 个第二个是Matcher断言第一个值匹配该匹配器通过doAssert生成带描述的AssertionError2 个第二个是普通值视为布尔表达式false则失败消息为第一个参数抛出AssertionError($args[0])3 个形如assertThat($identifier, $actual, $matcher)第三个参数若不是匹配器则自动包装为equalTo失败消息中带上$identifier第三种形态在源码注释中给出了完整示例// With an identifier assertThat(apple flavour, $apple-flavour(), equalTo(tasty)); // Without an identifier assertThat($apple-flavour(), equalTo(tasty)); // Evaluating a boolean expression assertThat(some error, $a $b); assertThat($a $b);其中“第三个参数若不是匹配器则自动包装为equalTo”这一行为由Util::wrapValueWithIsEqual($args[2])实现——这意味着你甚至可以省略equalTo()而直接传一个期望值。2.2 失败消息的生成机制当匹配失败时doAssert()MatcherAssert.php会构建一条极具可读性的失败描述Expected: 匹配器对期望的描述 but: 匹配器对实际值的失配描述其实现是用StringDescription依次追加标识符可选、Expected:、appendDescriptionOf($matcher)即匹配器的describeTo输出、换行后的but:最后调用$matcher-describeMismatch($actual, $description)描述实际值的失配原因。这也是 Hamcrest 与普通assertTrue最直观的差异——失败信息本身就是可读的英文句子便于快速定位问题。此外MatcherAssert 还维护了一个静态断言计数器$_count提供getCount()与resetCount()两个静态方法可统计/重置已执行的断言次数。三、与 Java API 的六点差异PHP 移植版的适配细节README 中最重要的技术内容是 hamcrest-php 相对原始 Java API 的六点差异。逐一结合仓库源码展开1.instanceOf($theClass)改名为anInstanceOf($theClass)Java 中的instanceOf是保留字PHP 的instanceof同样是语言关键字无法用作方法名因此移植时改名为anInstanceOf。对应实现位于 Core/IsInstanceOf.php并有配套测试 tests/Hamcrest/Core/IsInstanceOfTest.php 验证其行为。2.both(...)-and(...)改为both(...)-andAlso(...)Java 中and是关键字PHP 中and也是运算符不能作为方法名因此逻辑“与”组合改为andAlso。见 Core/CombinableMatcher.php/** Diversion from Hamcrest-Java... Logical and not permitted */ public function andAlso(Matcher $other) { return new self(new AllOf($this-_templatedListWith($other))); }源码注释直言这是“对 Hamcrest-Java 的偏离”Diversion from Hamcrest-Java。andAlso将当前匹配器与新匹配器组合成一个AllOf全满足匹配器继续包装回CombinableMatcher从而支持链式连续组合。3.either(...)-or(...)改为either(...)-orElse(...)同理or也是 PHP 运算符逻辑“或”组合改名orElse底层包装为AnyOf任一满足匹配器/** Diversion from Hamcrest-Java... Logical or not permitted */ public function orElse(Matcher $other) { return new self(new AnyOf($this-_templatedListWith($other))); }CombinableMatcher的类注释中还给出了两个组合用法的官方示例assertThat($string, both(containsString(a))-andAlso(containsString(b))); assertThat($string, either(containsString(a))-orElse(containsString(b)));4. 允许“PHP 式”动态类型但语义攸关的匹配器除外除非某个匹配器的语义本身与类型强相关否则 hamcrest-php 允许对输入采用 PHP 的动态类型dynamic typing处理。README 点名的两个例外是stringContains()字符串包含匹配器输入必须是字符串语义greaterThan()数值比较匹配器依赖类型比较。后者的类型约束在 Number/OrderingComparison.php 中有明确体现其构造函数调用parent::__construct(self::TYPE_NUMERIC)继承自类型安全匹配器TypeSafeMatcher只对数值类型执行matchesSafely()。该文件还完整实现了五个数值比较工厂comparesEqualTo($value) // 等于 greaterThan($value) // 大于 greaterThanOrEqualTo($value) // 大于等于factory atLeast lessThan($value) // 小于 lessThanOrEqualTo($value) // 小于等于factory atMost内部通过_compare()返回-1 / 0 / 1并夹在[$minCompare, $maxCompare]区间内判定失配时还能给出“was greater than / equal to / less than”的英文描述。5. 四个未移植的官方匹配器README 明确列出以下 Java 匹配器因“在 PHP 中无意义或不适用”而未移植Java 匹配器未移植原因README 语境typeCompatibleWith($theClass)依赖 Java 的类型系统语义eventFrom($source)面向 Java 事件对象PHP 无对应概念hasProperty($name)依赖 JavaBean 属性约定PHP 侧用 POPO 类比不成立samePropertyValuesAs($obj)同上依赖 JavaBean 属性约定其中hasProperty、samePropertyValuesAs的注释还带了一个开发者的幽默说明除非把 PHP 的 POPOPlain Old PHP Objects类比成 Java 的 POJO/JavaBeans——而这在 PHP 中并不成立因此没有移植。6. 集合匹配器未来将提供 PHP 专属别名README 说明由于 Java 的 Arrays、Collections、Sets、Maps 与 PHP 的数组在命名习惯上的差异当大部分集合匹配器最终移植完成后大概率会为它们创建 PHP 特有的别名。实际上从 Matchers.php 的静态工厂可以看到这种“一义两名”的别名机制已经开始落地例如anArray()与arrayContainingInAnyOrder()对应的别名containsInAnyOrder()arrayContaining()对应的别名contains()hasItemInArray()对应的别名hasValue()hasKeyInArray()对应的别名hasKey()。这些别名都只是转发到同一个底层实现如hasValue内部调用IsArrayContaining::hasItemInArray体现了“Java 风格命名 PHP 风格别名”并存的策略。四、匹配器工厂体系Hamcrest\Matchers的分类目录Matchers.php共 713 行是所有匹配器的静态工厂集合文件头注释说明它是“从静态方法factorydoctag 自动生成的”。结合hamcrest/Hamcrest/目录的源码组织可以将全部匹配器按语义分类如下数组ArraysanArray()、arrayContaining()/contains()按序包含、arrayContainingInAnyOrder()/containsInAnyOrder()乱序包含、hasItemInArray()/hasValue()、hasKeyInArray()/hasKey()、hasKeyValuePair()、arrayWithSize()。实现位于 Arrays/ 下的 8 个类其中MatchingOnce、SeriesMatchingOnce负责“每个元素只匹配一次”的乱序语义。集合Collectionempty()对应IsEmptyTraversable、nonEmpty()、hasSize()对应IsTraversableWithSize、hasItem()/hasItems()对应IsCollectionContaining且非匹配器参数会自动降级为equalTo见 Matchers.php 中的示例assertThat(array(a, b), hasItem(b))。核心CoreallOf()、anyOf()、both()/either()组合、describedAs()、everyItem()、is()、anything()、not()、nullValue()/notNullValue()、equalTo()、identicalTo()、sameInstance()、anInstanceOf()/anyOf()等。其中is($value)在$value不是匹配器时自动包装为equalTo($value)因此assertThat($cheese, is(equalTo($smelly)))与assertThat($cheese, is($smelly))等价Matchers.php。数值NumbercloseTo($value, $delta)近似相等IsCloseTo、comparesEqualTo()、greaterThan()、greaterThanOrEqualTo()、lessThan()、lessThanOrEqualTo()OrderingComparison类型安全。文本TextisEmptyString()、equalToIgnoringCase()、equalToIgnoringWhiteSpace()、matchesPattern()、containsString()、containsStringIgnoringCase()、stringContainsInOrder()、startsWith()、endsWith()。实现类位于 Text/SubstringMatcher是各字符串包含类别的公共抽象基类。类型TypearrayValue()、booleanValue()、callableValue()、doubleValue()、integerValue()/intValue()、numericValue()、objectValue()、resourceValue()、scalarValue()、stringValue()。以 Type/IsInteger.php 为例它继承IsTypeOf并在构造时传入integerintegerValue()工厂方法上标注factory intValue说明Matchers::intValue()与Matchers::integerValue()是同一匹配器的两个入口。XMLhasXPath()Xml/HasXPath.php配合 XPath 表达式校验 XML 内容。五、匹配器与描述机制的架构Matcher 接口与基类要真正理解 hamcrest-php需要掌握其最核心的接口与抽象类Matcher接口核心方法是matches($item)返回布尔、describeTo(Description $description)描述“期望什么”、describeMismatch($item, Description $description)描述“实际是什么”。BaseMatcherBaseMatcher.php所有匹配器的默认基类提供默认的describeMismatch输出was 值以及__toString()借助StringDescription::toString将自身描述转为字符串。TypeSafeMatcher/TypeSafeDiagnosingMatcher类型安全匹配器基类先校验类型再执行matchesSafely()OrderingComparison、IsInteger等均继承自它们。FeatureMatcher用于“先提取对象某个特征值、再用子匹配器断言该特征值”的组合型匹配器。Description/StringDescription/NullDescription/BaseDescription/SelfDescribing描述机制的完整实现负责将匹配器与失配信息渲染成人类可读文本。AssertionError断言失败时抛出的异常类型扩展自 PHP 内置ErrorException或类似错误类。这一架构保证了任何自定义匹配器只要实现Matcher接口通常继承BaseMatcher或TypeSafeMatcher即可无缝接入assertThat、allOf、hasItem等所有组合场景并获得一致的失败消息输出。六、测试与验证仓库中的配套测试套件hamcrest-php 在 tests/ 目录提供了与实现一一对应的 PHPUnit 测试套件是学习每个匹配器行为的最佳参考资料tests/Hamcrest/Core/覆盖AllOf、AnyOf、CombinableMatcher、IsEqual、IsInstanceOf、IsNot、IsNull、IsTypeOf、Set等核心匹配器其中SampleBaseClass.php/SampleSubClass.php用于anInstanceOf的继承关系验证。tests/Hamcrest/Text/覆盖IsEmptyString、IsEqualIgnoringCase、IsEqualIgnoringWhiteSpace、MatchesPattern、StringContains含 IgnoringCase、InOrder、StringStartsWith、StringEndsWith等文本匹配器。tests/Hamcrest/Type/覆盖全部 10 个类型匹配器IsArrayTest、IsIntegerTest、IsNumericTest等。tests/Hamcrest/Number/IsCloseToTest、OrderingComparisonTest验证数值比较语义。tests/Hamcrest/Array/与Collection/验证数组与可遍历集合的匹配行为。tests/Hamcrest/Xml/HasXPathTest.php验证 XPath 匹配。基础设施tests/AbstractMatcherTest.php自定义匹配器的测试基类、tests/MatcherAssertTest.php、tests/StringDescriptionTest.php、tests/UtilTest.php以及tests/phpunit.xml.dist与tests/bootstrap.php。以 CombinableMatcherTest 为例它直接验证了 README 中第 2、3 点差异的行为both(...)-andAlso(...)必须两个条件同时成立either(...)-orElse(...)只要一个成立即可。这些测试即为“直译 Java API 但调整命名”这一事实的最直接证据。七、在 Laravel 测试栈中的落地方式回到本仓库的 Laravel 示例项目hamcrest-php 作为 vendor 依赖随 Composer 安装到vendor/hamcrest/hamcrest-php/。虽然项目根 composer.json 未直接声明 hamcrest但它通过phpunit/phpunit、phpspec/phpspec、mockery/mockery等require-dev依赖被传递引入。实际使用时可遵循以下路径安装composer require --dev hamcrest/hamcrest-php本仓库已随 vendor 自带无需重复安装。引入入口文件Composer 的filesautoload 会自动加载hamcrest/Hamcrest.php其中定义了Hamcrest_MatcherAssert、Hamcrest_Matchers这类下划线风格的全局别名类因此即使不写use语句也能直接使用Hamcrest_Matchers::equalToIgnoringCase(...)。在 PHPUnit 测试中使用在 Laravel 的tests/目录下直接调用assertThat($actual, Matchers::xxx(...))即可获得与 Java JUnit/Hamcrest 一致的断言体验也可以与 Mockery 的shouldReceive()-with(Matchers::xxx())结合用匹配器描述 mock 方法的期望参数。一个典型的完整示例use Hamcrest\Matchers; // 组合断言字符串同时包含 a 与 b对应差异 2 的 andAlso assertThat($string, Matchers::both(Matchers::containsString(a)) -andAlso(Matchers::containsString(b))); // 集合断言数组中包含某个元素非匹配器值自动降级为 equalTo assertThat(array(a, b), Matchers::hasItem(b)); // 类型断言值必须是整数 assertThat($count, Matchers::integerValue()); // 数值断言值必须大于 10 assertThat($total, Matchers::greaterThan(10));结语hamcrest-php 虽然只是 sql-server-samples Laravel 示例中的一个第三方测试依赖但它完整继承了 Hamcrest“用匹配器描述期望、用可读消息报告失败”的设计哲学并针对 PHP 语言特性做出了六处关键适配。理解这些适配anInstanceOf、andAlso、orElse、动态类型边界、未移植匹配器、集合别名与 MatcherAssert 的底层实现你就能在 Laravel/PHPUnit 测试中写出既接近自然语言、又可灵活组合的强表达力断言同时具备阅读和自定义匹配器源码的能力。【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表