深入解读 hamcrest-php:Laravel 项目中的 Hamcrest 匹配器 PHP 移植版
2026/9/23 16:25:23 网站建设 项目流程

深入解读 hamcrest-php:Laravel 项目中的 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.0phpspec/phpspec: ~2.1mockery/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 个直接断言布尔表达式为真抛出无消息的AssertionError
2 个(第二个是Matcher断言第一个值匹配该匹配器通过doAssert生成带描述的AssertionError
2 个(第二个是普通值)视为布尔表达式,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 属性约定

其中hasPropertysamePropertyValuesAs的注释还带了一个开发者的幽默说明:除非把 PHP 的 POPO(Plain 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/目录的源码组织,可以将全部匹配器按语义分类如下:

数组(Arrays)

  • anArray()arrayContaining()/contains()(按序包含)、arrayContainingInAnyOrder()/containsInAnyOrder()(乱序包含)、hasItemInArray()/hasValue()hasKeyInArray()/hasKey()hasKeyValuePair()arrayWithSize()
  • 实现位于 Arrays/ 下的 8 个类,其中MatchingOnceSeriesMatchingOnce负责“每个元素只匹配一次”的乱序语义。

集合(Collection)

  • empty()(对应IsEmptyTraversable)、nonEmpty()hasSize()(对应IsTraversableWithSize)、hasItem()/hasItems()(对应IsCollectionContaining,且非匹配器参数会自动降级为equalTo,见 Matchers.php 中的示例assertThat(array('a', 'b'), hasItem('b')))。

核心(Core)

  • allOf()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)。

数值(Number)

  • closeTo($value, $delta)(近似相等,IsCloseTo)、comparesEqualTo()greaterThan()greaterThanOrEqualTo()lessThan()lessThanOrEqualTo()OrderingComparison,类型安全)。

文本(Text)

  • isEmptyString()equalToIgnoringCase()equalToIgnoringWhiteSpace()matchesPattern()containsString()containsStringIgnoringCase()stringContainsInOrder()startsWith()endsWith()
  • 实现类位于 Text/,SubstringMatcher是各字符串包含类别的公共抽象基类。

类型(Type)

  • arrayValue()booleanValue()callableValue()doubleValue()integerValue()/intValue()numericValue()objectValue()resourceValue()scalarValue()stringValue()
  • 以 Type/IsInteger.php 为例,它继承IsTypeOf并在构造时传入'integer'integerValue()工厂方法上标注@factory intValue,说明Matchers::intValue()Matchers::integerValue()是同一匹配器的两个入口。

XML

  • hasXPath()Xml/HasXPath.php),配合 XPath 表达式校验 XML 内容。

五、匹配器与描述机制的架构:Matcher 接口与基类

要真正理解 hamcrest-php,需要掌握其最核心的接口与抽象类:

  • Matcher接口:核心方法是matches($item)(返回布尔)、describeTo(Description $description)(描述“期望什么”)、describeMismatch($item, Description $description)(描述“实际是什么”)。
  • BaseMatcher(BaseMatcher.php):所有匹配器的默认基类,提供默认的describeMismatch(输出was <值>)以及__toString()(借助StringDescription::toString将自身描述转为字符串)。
  • TypeSafeMatcher/TypeSafeDiagnosingMatcher:类型安全匹配器基类,先校验类型再执行matchesSafely()OrderingComparisonIsInteger等均继承自它们。
  • FeatureMatcher:用于“先提取对象某个特征值、再用子匹配器断言该特征值”的组合型匹配器。
  • Description/StringDescription/NullDescription/BaseDescription/SelfDescribing:描述机制的完整实现,负责将匹配器与失配信息渲染成人类可读文本。
  • AssertionError:断言失败时抛出的异常类型,扩展自 PHP 内置ErrorException或类似错误类。

这一架构保证了:任何自定义匹配器只要实现Matcher接口(通常继承BaseMatcherTypeSafeMatcher),即可无缝接入assertThatallOfhasItem等所有组合场景,并获得一致的失败消息输出。


六、测试与验证:仓库中的配套测试套件

hamcrest-php 在 tests/ 目录提供了与实现一一对应的 PHPUnit 测试套件,是学习每个匹配器行为的最佳参考资料:

  • tests/Hamcrest/Core/:覆盖AllOfAnyOfCombinableMatcherIsEqualIsInstanceOfIsNotIsNullIsTypeOfSet等核心匹配器;其中SampleBaseClass.php/SampleSubClass.php用于anInstanceOf的继承关系验证。
  • tests/Hamcrest/Text/:覆盖IsEmptyStringIsEqualIgnoringCaseIsEqualIgnoringWhiteSpaceMatchesPatternStringContains(含 IgnoringCase、InOrder)、StringStartsWithStringEndsWith等文本匹配器。
  • tests/Hamcrest/Type/:覆盖全部 10 个类型匹配器(IsArrayTestIsIntegerTestIsNumericTest等)。
  • tests/Hamcrest/Number/IsCloseToTestOrderingComparisonTest验证数值比较语义。
  • tests/Hamcrest/Array/Collection/:验证数组与可遍历集合的匹配行为。
  • tests/Hamcrest/Xml/HasXPathTest.php:验证 XPath 匹配。
  • 基础设施:tests/AbstractMatcherTest.php(自定义匹配器的测试基类)、tests/MatcherAssertTest.phptests/StringDescriptionTest.phptests/UtilTest.php,以及tests/phpunit.xml.disttests/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/phpunitphpspec/phpspecmockery/mockeryrequire-dev依赖被传递引入。实际使用时可遵循以下路径:

  1. 安装composer require --dev hamcrest/hamcrest-php(本仓库已随 vendor 自带,无需重复安装)。
  2. 引入入口文件:Composer 的filesautoload 会自动加载hamcrest/Hamcrest.php,其中定义了Hamcrest_MatcherAssertHamcrest_Matchers这类下划线风格的全局别名类,因此即使不写use语句也能直接使用Hamcrest_Matchers::equalToIgnoringCase(...)
  3. 在 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 语言特性做出了六处关键适配。理解这些适配(anInstanceOfandAlsoorElse、动态类型边界、未移植匹配器、集合别名)与 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询