PHP Pest 插件开发:自定义 Expectation 与命令行输出的实践
大家好!今天我们来深入探讨如何使用 PHP 的 Pest 测试框架开发自定义插件,重点关注自定义 Expectation 和命令行输出的实践。Pest 提供了强大的扩展机制,允许我们根据项目需求定制测试行为和报告方式,从而提高测试效率和代码质量。
1. Pest 插件基础
Pest 插件本质上是一个 PHP 类,它必须继承 PestPlugin 抽象类。这个抽象类定义了插件的基本接口,包括插件的注册和生命周期管理。
<?php
namespace MyCustomPlugin;
use PestPlugin;
class MyPlugin extends Plugin
{
public function boot(): void
{
// 插件启动时执行的代码,例如注册自定义 Expectation
}
}
boot() 方法是插件的核心入口,在这里我们可以注册自定义 Expectation、添加命令行选项、监听 Pest 事件等。
2. 自定义 Expectation
Expectation 是 Pest 中用于断言的核心机制。Pest 提供了丰富的内置 Expectation,但有时我们需要根据业务逻辑定义自己的 Expectation,以更精确地表达测试意图。
2.1 创建自定义 Expectation 类
自定义 Expectation 类需要实现 PestExpectation 接口。这个接口定义了一个 __call() 方法,用于处理动态调用的方法。
<?php
namespace MyCustomPluginExpectations;
use PestExpectation;
use PHPUnitFrameworkAssert;
class ContainsJson extends Expectation
{
public function __call(string $name, array $arguments): Expectation
{
if ($name === 'toContainJson') {
return $this->toContainJson(...$arguments);
}
return parent::__call($name, $arguments);
}
public function toContainJson(array $expectedJson): Expectation
{
$actualJson = json_decode($this->value, true);
Assert::assertIsArray($actualJson, 'Value is not a valid JSON string.');
Assert::assertIsArray($expectedJson, 'Expected value is not a valid JSON array.');
foreach ($expectedJson as $key => $value) {
Assert::assertArrayHasKey($key, $actualJson, "Key '{$key}' not found in JSON.");
Assert::assertEquals($value, $actualJson[$key], "Value for key '{$key}' does not match.");
}
return $this;
}
}
这个 ContainsJson Expectation 允许我们断言一个字符串是否包含指定的 JSON 数据。__call() 方法拦截了 toContainJson 方法的调用,并将控制权交给 toContainJson() 方法处理实际的断言逻辑。
2.2 注册自定义 Expectation
在插件的 boot() 方法中,我们需要注册自定义 Expectation,将其关联到 Pest 的 Expectation 体系中。
<?php
namespace MyCustomPlugin;
use PestPlugin;
use MyCustomPluginExpectationsContainsJson;
class MyPlugin extends Plugin
{
public function boot(): void
{
$this->expectations([
'string' => ContainsJson::class,
]);
}
}
$this->expectations() 方法接收一个数组,键表示 Pest 的 Expectation 类型(例如 string、array、int),值表示对应的自定义 Expectation 类。
现在,我们就可以在测试用例中使用自定义 Expectation 了:
<?php
it('contains the expected JSON data', function () {
$jsonString = '{"name": "John Doe", "age": 30, "city": "New York"}';
expect($jsonString)->toContainJson(['name' => 'John Doe', 'age' => 30]);
});
it('fails if the JSON string is invalid', function () {
$invalidJsonString = '{"name": "John Doe", "age": 30, "city": "New York';
expect($invalidJsonString)->toContainJson(['name' => 'John Doe', 'age' => 30]);
})->throws(PHPUnitFrameworkAssertionFailedError::class);
it('fails if the expected JSON is not found', function () {
$jsonString = '{"name": "John Doe", "age": 30, "city": "New York"}';
expect($jsonString)->toContainJson(['name' => 'Jane Doe', 'age' => 30]);
})->throws(PHPUnitFrameworkAssertionFailedError::class);
it('fails if the expected JSON value does not match', function () {
$jsonString = '{"name": "John Doe", "age": 30, "city": "New York"}';
expect($jsonString)->toContainJson(['name' => 'John Doe', 'age' => 31]);
})->throws(PHPUnitFrameworkAssertionFailedError::class);
3. 命令行输出定制
Pest 允许我们定制命令行输出,以便在测试过程中提供更详细的信息或自定义报告。
3.1 监听 Pest 事件
Pest 提供了丰富的事件系统,允许我们在测试的不同阶段执行自定义代码。我们可以监听这些事件,并在命令行输出自定义信息。
<?php
namespace MyCustomPlugin;
use PestEventsTestPassed;
use PestPlugin;
use SymfonyComponentConsoleOutputOutputInterface;
class MyPlugin extends Plugin
{
public function boot(): void
{
TestPassed::subscribe(function (TestPassed $event, OutputInterface $output) {
$testName = $event->test->description;
$output->writeln("<info>Test Passed: {$testName}</info>");
});
}
}
TestPassed::subscribe() 方法注册一个事件监听器,当测试通过时,该监听器会被调用。监听器接收一个 TestPassed 事件对象和一个 OutputInterface 对象,我们可以使用 OutputInterface 对象将信息输出到命令行。
这个例子中,我们在测试通过时,将测试名称以绿色字体输出到命令行。
3.2 添加命令行选项
Pest 允许我们添加自定义命令行选项,以便在运行测试时控制插件的行为。
<?php
namespace MyCustomPlugin;
use PestConsoleArguments;
use PestPlugin;
use SymfonyComponentConsoleInputInputOption;
class MyPlugin extends Plugin
{
public function boot(): void
{
Arguments::addOption('log-slow', null, InputOption::VALUE_NONE, 'Log slow tests');
$this->afterEach(function () {
if (Arguments::has('log-slow') && $this->time() > 0.5) {
echo "Test {$this->description()} took {$this->time()} seconds.n";
}
});
}
}
Arguments::addOption() 方法用于添加命令行选项。这个例子中,我们添加了一个名为 log-slow 的选项,用于记录执行时间超过 0.5 秒的测试。
$this->afterEach() 方法注册一个回调函数,该回调函数在每个测试用例执行后被调用。在回调函数中,我们检查 log-slow 选项是否被启用,以及测试执行时间是否超过 0.5 秒。如果条件满足,我们就将测试名称和执行时间输出到命令行。
现在,我们可以使用 --log-slow 选项来运行测试:
./vendor/bin/pest --log-slow
4. 完整示例:性能监控插件
让我们创建一个更完整的示例,一个性能监控插件,它可以记录每个测试的执行时间,并将执行时间超过阈值的测试报告出来。
<?php
namespace MyPerformancePlugin;
use PestEventsTestPassed;
use PestEventsTestFailed;
use PestPlugin;
use SymfonyComponentConsoleOutputOutputInterface;
use SymfonyComponentConsoleInputInputOption;
use PestConsoleArguments;
class PerformanceMonitor extends Plugin
{
private float $threshold;
public function boot(): void
{
// 添加命令行选项,用于设置性能阈值
Arguments::addOption('performance-threshold', null, InputOption::VALUE_REQUIRED, 'Performance threshold in seconds', 1.0);
// 获取命令行选项的值
$this->threshold = (float) Arguments::get('performance-threshold');
// 监听测试通过事件
TestPassed::subscribe(function (TestPassed $event, OutputInterface $output) {
$executionTime = $event->test->time();
$testName = $event->test->description;
if ($executionTime > $this->threshold) {
$output->writeln("<bg=yellow;fg=black>SLOW TEST:</> <comment>{$testName}</comment> took <info>{$executionTime} seconds</info>");
}
});
// 监听测试失败事件 (可选,也可以记录失败测试的性能)
TestFailed::subscribe(function (TestFailed $event, OutputInterface $output) {
$executionTime = $event->test->time();
$testName = $event->test->description;
if ($executionTime > $this->threshold) {
$output->writeln("<bg=red;fg=white>SLOW FAILED TEST:</> <comment>{$testName}</comment> took <info>{$executionTime} seconds</info>");
}
});
}
}
这个插件做了以下事情:
- 添加了一个名为
performance-threshold的命令行选项,用于设置性能阈值。默认值为 1.0 秒。 - 监听
TestPassed事件,当测试通过时,检查其执行时间是否超过阈值。如果超过阈值,就将测试名称和执行时间以黄色背景输出到命令行。 - 监听
TestFailed事件 (可选),当测试失败时,检查其执行时间是否超过阈值。如果超过阈值,就将测试名称和执行时间以红色背景输出到命令行。
要使用这个插件,首先确保你的 composer.json 文件中包含 my-performance-plugin/performance-monitor 依赖,并且在 pest.php 文件中注册了该插件。
然后,你可以运行以下命令来运行测试,并设置性能阈值为 0.5 秒:
./vendor/bin/pest --performance-threshold=0.5
5. 表格总结
| 功能 | 描述 | 实现方式 |
|---|---|---|
| 自定义 Expectation | 扩展 Pest 的断言能力,根据业务逻辑定义自己的断言。 | 1. 创建实现 PestExpectation 接口的类。2. 在 __call() 方法中拦截自定义方法调用。3. 在自定义方法中执行断言逻辑,使用 PHPUnitFrameworkAssert 类。4. 在插件的 boot() 方法中使用 $this->expectations() 方法注册自定义 Expectation。 |
| 命令行输出定制 | 定制 Pest 的命令行输出,以便在测试过程中提供更详细的信息或自定义报告。 | 1. 监听 Pest 事件,例如 TestPassed、TestFailed。2. 在事件监听器中使用 SymfonyComponentConsoleOutputOutputInterface 对象将信息输出到命令行。3. 使用 PestConsoleArguments 类添加自定义命令行选项。4. 在事件监听器或测试用例中使用 Arguments::has() 和 Arguments::get() 方法获取命令行选项的值。 |
| 性能监控插件 | 记录每个测试的执行时间,并将执行时间超过阈值的测试报告出来。 | 1. 添加 performance-threshold 命令行选项。2. 监听 TestPassed 和 TestFailed 事件。3. 获取测试用例的执行时间。 4. 如果执行时间超过阈值,则将测试用例名称和执行时间输出到命令行。 |
6. 插件开发最佳实践
- 命名空间: 使用独立的命名空间来组织插件代码,避免与其他代码冲突。
- 单一职责: 插件应该只负责一个特定的功能,避免过度设计。
- 可配置性: 允许用户通过命令行选项或配置文件来定制插件的行为。
- 测试: 为插件编写单元测试,确保其功能正常。
- 文档: 提供清晰的文档,说明插件的功能、用法和配置选项。
7. 深入理解 Pest 插件机制
Pest 的插件机制基于事件驱动架构。Pest 在测试的不同阶段触发各种事件,例如 BeforeAll、BeforeEach、TestPassed、TestFailed、AfterEach、AfterAll。插件可以监听这些事件,并在事件发生时执行自定义代码。
Pest 使用 SymfonyComponentEventDispatcherEventDispatcher 组件来实现事件分发。我们可以使用 PestEvents 命名空间下的类来订阅事件。
8. Pest 插件的发布与共享
开发完成的 Pest 插件可以发布到 Packagist,以便其他开发者使用。
发布插件的步骤如下:
- 在
composer.json文件中定义插件的元数据,例如名称、描述、作者、许可证等。 - 将插件代码推送到 GitHub 或其他代码托管平台。
- 在 Packagist 上注册插件。
9. 总结:打造高效、可维护的测试流程
通过自定义 Expectation,我们能更精确地表达测试意图,提高测试代码的可读性。通过定制命令行输出,我们能更好地了解测试过程,及时发现问题。结合 Pest 的强大功能和灵活的扩展机制,我们可以打造高效、可维护的测试流程,保障代码质量。
希望今天的分享对大家有所帮助!谢谢!