#1888·pest

`expect()->toBeInstanceOf()` / `not->toBeNull()` / `toBeString()` 等不像 PHPUnit 的 assert 方法那样限制类型

作者: sebj54创建于 2026年8月25日更新于 2026年9月2日

这个问题是借助 Claude 的帮助解决的,但这是一个真正的人在询问 背景

PHPUnit 的 assertInstanceOf(), assertNotNull(), assertIsString()/Int/Float/Array/Numericphpunit/phpunitAssert 类中直接带有 @phpstan-assert 注解(例如,在 assertNotNull() 上的 @phpstan-assert null $actual)。PHPStan 的核心会直接读取这些内容,并在每一行后为变量的类型在 Scope 中进行窄化。Pest 的等价物(expect($x)->toBeInstanceOf(Y::class), expect($x)->not->toBeNull(), expect($x)->toBeString(), …) 并没有这样做。我检查了 pest-plugin-phpstan 的源代码,发现了 RedundantExpectationRule / ImpossibleExpectationRule(由 MatcherAssertionRegistry 提供支持),这些规则会读取范围中已知的类型,以标记一个冗余/不可能的预期值 - 但该包中的任何类都没有实现 PHPStan 的 TypeSpecifyingExtension,因此从来没有将窄化的类型写回范围中。 影响

在一个大型测试套件(例如 Laravel/Larastan, 级别 9)中,将 $this->assertInstanceOf(...) / $this->assertNotNull(...) 转换为它们的 Pest expect() 等价物会导致后续每一行的静态分析出现问题,这些行会读取现在已不再窄化的值(例如, "无法访问属性 $x 在 Y/null 上", "… 在混合类型上", 等等)。我们最终不得不在每个被断言的值再次读取的地方保留约 130 个 PHPUnit 样式的断言,以此作为理由。 问题

这是否是一个故意的范围限制,还是可以通过 TypeSpecifyingExtensionMatcherAssertionRegistry::METHOD_ASSERTIONS 中的匹配器(如 toBeString, toBeInt, toBeFloat, toBeArray, toBeInstanceOf, not->toBeNull, …) 来实现?如果需要,我很乐意帮助调查/贡献 - 只是想先确认是否存在已知的原因(例如, ->not 链,或高阶测试,使这种实现在一般情况下是不安全的),然后再寻找 PR。