CatchAdmin PHP 后台管理框架 Logo CatchAdmin

PHP 测试框架 Pest 5 发布 新增测试影响分析、Agent 验证与 Evals

Pest PHP 的下一个重要版本 Pest v5 现已发布。Nuno Maduro 在波士顿举行的 Laracon US 2026 大会上宣布了 Pest 5,并在大会期间标记了 v5.0.0 版本。该版本引入了 TIA 引擎,可大幅缩短测试套件的执行时间。Pest 5 要求 PHP 8.4 与 PHPUnit 13,同时汇集了一系列面向 AI Agents、PHPStan、Rector 等的第一方插件。

TIA 引擎从根本上改变了测试速度。Laravel Cloud 的测试套件包含 19,000 余个测试,执行时间从 3 分钟缩短到 5 秒:

以下是 Pest 5 的新特性一览:

  • TIA 引擎:仅重新运行受最新改动影响的测试,其余测试从缓存中回放,同时不牺牲覆盖率准确性
  • Agent 插件:为 AI 编码 Agent 提供一条命令,使其在真实测试套件中检查改动是否真正生效
  • Evals 插件:通过你已在使用的 expect() API,以确定性检查与 AI 评分器对 LLM 输出进行评分
  • PHPStan 插件:让 PHPStan 认识 it()、test()、expect() 以及测试闭包内的 $this
  • Rector 插件:提供 60 条规则,将原始 PHP 断言改写为 Pest 匹配器,并处理主版本升级
  • 新增针对邮箱、ULID、IP 地址等手工校验繁琐格式的预期断言
  • 以 PHP 8.4 与 PHPUnit 13 作为新的基线

新特性

使用 TIA 引擎进行测试影响分析

TIA 是 Test Impact Analysis(测试影响分析)的缩写。首次运行会记录哪些测试触及了哪些文件,之后的每次运行只执行受改动影响的测试,其余测试则回放缓存结果。

在任何命令调用中加入 --tia 即可:

bash
./vendor/bin/pest --parallel --tia

汇总信息会显示本次运行的构成(受影响、未缓存、回放各多少):

text
Tests:    774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)
Duration: 3.92s

回放并不是跳过测试。每条缓存结果都保存了测试产生的全部内容,精确到其覆盖的行与分支,因此 --coverage 报告与 --min 阈值的行为与完整运行整个测试套件时一致。

该功能的特别之处在于,依赖图能感知的不只是 PHP 文件。改动一个迁移文件,Pest 只会重新运行查询过该表的测试;编辑一个共享 JS 组件,它会遍历 Vite 的模块图,找出引入该组件的 Inertia 页面;修改一个 Blade 模板,它会重新运行渲染过该模板的测试。它通过 Composer 识别 Laravel、Symfony、Livewire、Inertia 以及浏览器资源,因此无需任何配置。

Pest 还会在哈希前对文件进行规范化处理,去除空白、注释与文档块。一次 Pint 格式化或一次 README 编辑会产生相同的哈希,因而不会触发任何测试运行。

注意:记录基线需要覆盖率驱动程序,因此你需要安装 PCOV 或 Xdebug。在大型测试套件上,首次运行需要较长时间,因此团队可让 CI 在每次合并到 main 时记录一次基线,其他成员直接下载即可。

你可以在 tests/Pest.php 中配置相关行为:

php
pest()->tia()
    ->always()     // run TIA on every invocation
    ->locally()    // restrict always() to local environments only
    ->baselined()  // fetch shared baseline from CI
    ->filtered();  // load only affected test files

使用 Agent 插件验证 AI Agent 的改动

编码 Agent 编写代码驾轻就熟,但缺乏确认代码能否正常工作的有效途径。Agent 插件为它们提供了这样的途径。将其作为开发依赖安装:

bash
composer require pestphp/pest-plugin-agent --dev

该插件新增了 --agent 选项,可在完整的 Pest 测试中运行一段代码片段,并且你的 factories、RefreshDatabase 与 Laravel fakes 可用状态与在功能测试中完全一致:

bash
./vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); $this->actingAs($user)->get("/dashboard")->assertOk();'

请使用单引号,以免 shell 对 $user 进行插值,并务必使用完整的类名。每段代码片段会以名为“verify”的独立测试运行,且 --agent 可以多次传入。

如果你安装了浏览器测试插件,同一个探针还可以驱动真实浏览器,并断言由此触发的后端副作用,而这些是仅检查 UI 所无法看到的:

bash
./vendor/bin/pest --agent='\Illuminate\Support\Facades\Mail::fake(); visit("/contact")->type("email", "test@example.com")->press("Send")->assertSee("Message sent");'

Pest 明确表示,该功能旨在提供工作过程中的快速反馈,而非取代提交到版本库中的回归测试。

使用 Evals 测试 LLM 输出

向同一模型发送两次相同的提示词,会得到两个不同的答案,这使得相等性断言几乎失去意义。Evals 插件改用你已在使用的 expect() API 对输出质量进行评分:

bash
composer require pestphp/pest-plugin-evals --dev
php
use App\Agents\CapitalCityAgent;
 
it('answers capital city questions correctly', function (): void {
    expect(CapitalCityAgent::class)
        ->prompt('What is the capital of France?')
        ->toContain('Paris')            // deterministic check
        ->toBeRelevant()                // LLM-as-judge scorer
        ->toBeSimilar('Paris, France'); // semantic similarity
});

每次评估(eval)都会调用真实模型,因此在常规运行中它们会被跳过,不会产生任何费用。需要时传入 --evals 即可:

bash
./vendor/bin/pest            # evals skipped, no API calls
./vendor/bin/pest --evals    # real model, all scorers active

确定性预期断言(toContain()、toMatch()、toBe()、toBeJson())完全不需要任何驱动程序。带评分功能的断言各自接受一个介于 0.0 与 1.0 之间的阈值,默认值为 0.7。其中,toBeRelevant() 用于根据提示词对响应进行评分,toBeSafe() 用于评估不安全内容以及模型对提示注入的抵抗能力,toBeFactual() 用于对照参考答案进行检查,toBeSimilar() 用于基于嵌入向量的相似度比较,toPassJudge() 用于按你用简单英语(大白话)写出的标准进行评判。此外,还有 toHaveToolCalls() 与 toFollowTrajectory(),用于确认 Agent 是否按正确顺序调用了正确的工具;repeat() 用于对同一提示词多次采样;如果你希望自行编写评分逻辑,则可以使用 toPassScorer()。

默认情况下,评分通过 Laravel AI 完成。评判器(judge)与嵌入向量(embeddings)驱动程序都接受闭包或自定义类,因此切换到其他提供商只需在 tests/Pest.php 中添加几行配置。

为 Pest 测试提供 PHPStan 支持

第一方 PHPStan 支持是社区长久以来呼声最高的功能之一。开箱即用的 PHPStan 不认识 it()、test() 或 expect(),也不清楚测试闭包内的 $this 指的是什么:

bash
composer require pestphp/pest-plugin-phpstan --dev
composer require phpstan/phpstan --dev

该插件会读取你的 Pest.php 配置来确定 $this 的类型,并同时支持 uses(TestCase::class)->in(...) 与 pest()->extend(...)->use(...)->in(...) 两种写法。如果你使用了 phpstan/extension-installer,它会自动完成注册;否则需要将其添加到 phpstan.neon 中:

yaml
includes:
    - vendor/pestphp/pest-plugin-phpstan/extension.neon

由此,类型信息会在断言链中流动:toBeInt() 能将 int|string 收窄,在 beforeEach() 中赋值的属性会在 $this 上获得类型,而 expect($user)->name->toBe('Nuno') 这类高阶预期断言则会从底层值解析类型。该插件还能捕获永远无法通过的预期断言:

php
expect(10)->toStartWith('1'); // int can never satisfy toStartWith()

在类型推断之外,该插件还提供了一系列感知 Pest 的规则,用于检查静态测试闭包、beforeAll() 内的 $this、重复的测试描述,以及无效的 throws() 与 covers() 引用。每条规则都有稳定的标识符,例如 pest.expectation.impossible,因此可以有选择地忽略它们。

使用 Rector 重构测试

Rector 插件内置了 60 条规则,分为编码风格(coding style)与版本升级(version upgrade)两组:

bash
composer require pestphp/pest-plugin-rector --dev
composer require rector/rector --dev

在 rector.php 中添加一个规则集:

php
use Pest\Rector\Set\PestSetList;
use Rector\Config\RectorConfig;
 
return RectorConfig::configure()
    ->withPaths([__DIR__ . '/tests'])
    ->withSets([
        PestSetList::CODING_STYLE,
    ]);

编码风格规则集将原始 PHP 断言改写为匹配器,并将冗余的预期断言链接起来:

diff
-expect(count($array))->toBe(5);
-expect(array_key_exists('id', $array))->toBeTrue();
+expect($array)->toHaveCount(5)
+    ->toHaveKey('id');

其他规则会将 PHPUnit 断言转换为 expect(),将 expect($value > 10)->toBeTrue() 改写为 toBeGreaterThan(10),将索引数组断言合并为 sequence(),并把 try/catch 异常测试替换为 toThrow()。PestLevelSetList 下的版本规则集用于处理主版本升级,且可累积应用。正式执行之前,请先用 vendor/bin/rector process --dry-run 预览所有改动。

时间均衡分片

时间均衡分片并非 5.0 的新特性,但如果你之前没有注意到,它值得再次提及,因为它在四月份悄然随 Pest v4.6.0 一同发布。按文件数量将测试套件拆分到多台 CI 机器上,往往会留下一个分片在其他分片早已完成后仍长时间运行。Pest 改为依据记录的运行时间来分配。

先记录一次各文件的运行耗时:

bash
./vendor/bin/pest --update-shards

然后将 tests/.pest/shards.json 提交到版本库,--shard 会自动读取其中的数据:

bash
./vendor/bin/pest --shard=1/4

在刷新耗时数据之前添加测试文件,测试套件依然可以正常运行。新文件会被均匀分配,而已知文件仍保持时间均衡,同时 Pest 会提醒你该分片文件(shards.json)已过期。

针对邮箱、ULID 与 IP 地址的新预期断言

八个新的匹配器覆盖了那些手工编写颇为繁琐的格式检查:

php
expect('nuno@pestphp.com')->toBeEmail();
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();
expect('192.168.1.1')->toBeIpAddress();
expect('00:1a:2b:3c:4d:5e')->toBeMacAddress();
expect('example.com')->toBeHostname();
expect('example.co.uk')->toBeDomain();
expect('Zm9vYmFy')->toBeBase64();
expect('deadbeef')->toBeHexadecimal();

每个匹配器都接受一个可选的自定义失败消息,并且可以使用 not 对其中任意一个取反。

升级到 Pest 5

Pest 5 要求 PHP 8.4 或更高版本,并基于 PHPUnit 13 运行。两者之中,升级过程中大部分麻烦来自 PHPUnit 13 本身,而非 Pest。建议阅读 PHPUnit 13 的变更日志,了解可能影响测试套件的内容。Pest 自身的升级指南显示,整个升级大约只需两分钟,且除版本号提升外没有记录任何 API 层面的破坏性变更。

对于大多数测试套件,只需要修改 composer.json 中的一行:

diff
-    "pestphp/pest": "^4.0",
+    "pestphp/pest": "^5.0",

顺手将 Pest 维护的插件一并升级到 ^5.0。在体验主打功能之前,有两点值得了解:TIA 引擎需要 PCOV 或 Xdebug 来记录基线,而 CI 基线共享要求已认证的 GitHub CLI,且仅适用于 GitHub。

参考资料

你可以在 Pest 官网阅读 Pest 5 的完整发布公告、升级指南,以及 TIA 引擎、Agent、Evals、PHPStan 和 Rector 插件的文档。

Pest 由 Nuno Maduro 创建并维护,你可以在 GitHub 上的 pestphp/pest 中找到 v5.0.0 版本发布、完整差异以及源代码。

本作品采用《CC 协议》,转载必须注明作者和本文链接