Pest PHP测试框架:简洁语法与自定义断言(Expectations)的实践应用
大家好,今天我们来深入探讨 Pest PHP 测试框架。 Pest 以其简洁的语法和强大的自定义能力,正日益受到 PHP 开发者的欢迎。我们将重点关注 Pest 的语法特性,以及如何通过自定义 Expectations 来扩展其断言能力,以适应各种复杂的测试场景。
Pest 简介与核心概念
Pest 是一个优雅的 PHP 测试框架,建立在 PHPUnit 之上。它旨在提供更简洁、更易读的测试语法,同时保留 PHPUnit 的强大功能。 Pest 通过引入 Expectations(期望)的概念,简化了断言的编写,并鼓励使用 Data Providers 进行数据驱动测试。
核心概念:
- Tests (测试): 独立的测试用例,用于验证特定代码的行为。
- Expectations (期望): Pest 提供的断言方法,用于验证测试结果是否符合预期。
- Data Providers (数据提供者): 用于提供测试数据,实现数据驱动测试。
- BeforeEach/AfterEach (前置/后置操作): 在每个测试用例执行前后执行的函数,用于设置和清理测试环境。
- Groups (分组): 将测试用例分组,方便批量执行特定类型的测试。
- Plugins (插件): 扩展 Pest 功能的工具,例如并行测试、覆盖率报告等。
Pest 的简洁语法
Pest 的语法旨在减少样板代码,使测试更加易读易懂。以下是一些关键的语法特性:
test()函数: 用于定义测试用例。expect()函数: 用于定义期望(断言)。beforeEach()/afterEach()函数: 用于定义前置和后置操作。- 箭头函数: 简化闭包函数的定义。
示例:
<?php
use function PestLaravelget;
it('returns a successful response', function () {
get('/')->assertStatus(200);
});
it('has the correct title', function () {
$response = get('/');
$response->assertSee('My Application');
});
describe('User Registration', function () {
beforeEach(function () {
$this->userData = [
'name' => 'John Doe',
'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
];
});
it('registers a new user', function () {
$this->post('/register', $this->userData)
->assertRedirect('/home');
$this->assertDatabaseHas('users', [
'email' => $this->userData['email'],
]);
});
it('requires a name', function () {
$userData = $this->userData;
unset($userData['name']);
$this->post('/register', $userData)
->assertSessionHasErrors('name');
});
});
代码解释:
it('returns a successful response', function () { ... });定义了一个测试用例,描述为 "returns a successful response"。get('/')->assertStatus(200);使用 Laravel 的测试助手get()发起一个 GET 请求到/,并使用assertStatus()断言响应状态码为 200。describe('User Registration', function () { ... });定义了一个测试套件,用于组织与用户注册相关的测试用例。beforeEach(function () { ... });在每个User Registration套件中的测试用例执行前,设置$this->userData。$this->post('/register', $this->userData)使用 Laravel 的测试助手post()发起一个 POST 请求到/register,并传递$this->userData作为请求参数。$this->assertDatabaseHas('users', [ ... ]);使用 Laravel 的测试助手assertDatabaseHas()断言数据库中存在满足指定条件的记录。
Pest 的 Expectations 断言
Pest 的 Expectations 是其核心特性之一,它提供了一种更简洁、更易读的方式来编写断言。 expect() 函数接受一个值,并返回一个 Expectation 对象,该对象提供了一系列方法用于执行断言。
常用 Expectations 方法:
| 方法 | 描述 | 示例 |
|---|---|---|
toBe($value) |
断言值等于 $value (使用 == 比较) |
expect($result)->toBe(10); |
toEqual($value) |
断言值等于 $value (使用 === 比较) |
expect($result)->toEqual('hello'); |
toBeTrue() |
断言值为 true |
expect($isValid)->toBeTrue(); |
toBeFalse() |
断言值为 false |
expect($isValid)->toBeFalse(); |
toBeNull() |
断言值为 null |
expect($value)->toBeNull(); |
toBeEmpty() |
断言值为空 (例如:空字符串、空数组) | expect($array)->toBeEmpty(); |
toBeString() |
断言值为字符串 | expect($name)->toBeString(); |
toBeInt() |
断言值为整数 | expect($age)->toBeInt(); |
toBeFloat() |
断言值为浮点数 | expect($price)->toBeFloat(); |
toBeArray() |
断言值为数组 | expect($items)->toBeArray(); |
toBeObject() |
断言值为对象 | expect($user)->toBeObject(); |
toBeInstanceOf($class) |
断言值为指定类的实例 | expect($user)->toBeInstanceOf(User::class); |
toContain($value) |
断言数组或字符串包含 $value |
expect($array)->toContain('item'); expect($string)->toContain('substring'); |
toHaveCount($count) |
断言数组或可数对象的元素个数为 $count |
expect($array)->toHaveCount(5); |
toThrow($exception) |
断言抛出指定异常 | expect(function() { throw new Exception(); })->toThrow(Exception::class); |
toMatch($pattern) |
断言字符串匹配指定正则表达式 | expect($string)->toMatch('/[a-z]+/'); |
toBeGreaterThan($value) |
断言值大于 $value |
expect($age)->toBeGreaterThan(18); |
toBeLessThan($value) |
断言值小于 $value |
expect($age)->toBeLessThan(65); |
示例:
<?php
it('checks if a number is greater than 5', function () {
$number = 10;
expect($number)->toBeGreaterThan(5);
});
it('checks if a string contains a substring', function () {
$string = 'Hello World';
expect($string)->toContain('World');
});
it('checks if an array has a specific count', function () {
$array = [1, 2, 3];
expect($array)->toHaveCount(3);
});
it('checks if an exception is thrown', function () {
expect(function () {
throw new Exception('This is an exception');
})->toThrow(Exception::class);
});
自定义 Expectations
Pest 允许我们自定义 Expectations,以满足特定的测试需求。这使得我们可以创建更具表达力的断言,并减少重复代码。
创建自定义 Expectation:
- 创建 Expectation 类: 创建一个新的类,并继承自
PestExpectation。 - 定义断言方法: 在类中定义新的断言方法。
- 注册 Expectation 类: 在
tests/Pest.php文件中注册自定义 Expectation 类。
示例:
假设我们需要一个断言来验证字符串是否为有效的邮箱地址。
1. 创建 Expectation 类 (tests/Expectations/toBeValidEmail.php):
<?php
namespace TestsExpectations;
use PestExpectation;
class toBeValidEmail
{
public function __invoke(Expectation $expectation): Expectation
{
$value = $expectation->value;
$isValid = filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
expect($isValid)->toBeTrue(); // 这里仍然使用Pest内置的expect
return $expectation;
}
}
2. 注册 Expectation 类 (tests/Pest.php):
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to define your test cases.
| You may use Test::uses(...) to declare traits.
|
*/
use TestsTestCase;
use TestsExpectationstoBeValidEmail; // 引入自定义 Expectation
uses(TestCase::class)->in('Feature', 'Unit');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things about values.
|
*/
expect()->extend('toBeValidEmail', new toBeValidEmail());
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful, you may need to add some helper functions to your tests.
| You can define them here.
|
*/
3. 使用自定义 Expectation:
<?php
it('checks if a string is a valid email address', function () {
$email = '[email protected]';
expect($email)->toBeValidEmail();
$invalidEmail = 'invalid-email';
expect(function () use ($invalidEmail) {
expect($invalidEmail)->toBeValidEmail();
})->toThrow(InvalidArgumentException::class); // 或者 AssertionFailedError,取决于内部实现和Pest版本
});
代码解释:
toBeValidEmail类实现了__invoke方法,该方法接受一个Expectation对象作为参数。__invoke方法获取Expectation对象的值 ($expectation->value),并使用filter_var函数验证其是否为有效的邮箱地址。expect($isValid)->toBeTrue();使用内置的toBeTrue()断言来验证$isValid的值。expect()->extend('toBeValidEmail', new toBeValidEmail());在tests/Pest.php文件中注册自定义 Expectation。- 现在,我们可以在测试用例中使用
expect($email)->toBeValidEmail();来验证邮箱地址的有效性。
更复杂的自定义 Expectation 示例:
假设我们需要验证一个数组是否包含指定的所有键。
1. 创建 Expectation 类 (tests/Expectations/toHaveKeys.php):
<?php
namespace TestsExpectations;
use PestExpectation;
use InvalidArgumentException;
class toHaveKeys
{
public function __invoke(Expectation $expectation, array $keys): Expectation
{
$array = $expectation->value;
if (!is_array($array)) {
throw new InvalidArgumentException('Expected value to be an array.');
}
foreach ($keys as $key) {
if (!array_key_exists($key, $array)) {
expect(array_key_exists($key, $array))->toBeTrue("Array is missing key: {$key}"); // 使用带有自定义消息的toBeTrue
}
}
return $expectation;
}
}
2. 注册 Expectation 类 (tests/Pest.php):
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to define your test cases.
| You may use Test::uses(...) to declare traits.
|
*/
use TestsTestCase;
use TestsExpectationstoHaveKeys; // 引入自定义 Expectation
uses(TestCase::class)->in('Feature', 'Unit');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things about values.
|
*/
expect()->extend('toHaveKeys', new toHaveKeys());
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful, you may need to add some helper functions to your tests.
| You can define them here.
|
*/
3. 使用自定义 Expectation:
<?php
it('checks if an array has specific keys', function () {
$array = ['name' => 'John Doe', 'email' => '[email protected]'];
expect($array)->toHaveKeys(['name', 'email']);
$array = ['name' => 'John Doe'];
expect(function () use ($array) {
expect($array)->toHaveKeys(['name', 'email']);
})->toThrow(InvalidArgumentException::class); // 或者 AssertionFailedError,取决于内部实现和Pest版本
});
代码解释:
toHaveKeys类实现了__invoke方法,该方法接受一个Expectation对象和一个键数组作为参数。__invoke方法首先验证Expectation对象的值是否为数组。- 然后,它遍历键数组,并使用
array_key_exists函数检查每个键是否存在于数组中。 如果某个键不存在,则抛出一个InvalidArgumentException异常。 - 现在,我们可以在测试用例中使用
expect($array)->toHaveKeys(['name', 'email']);来验证数组是否包含指定的键。
自定义 Expectation 的优势:
- 提高代码可读性: 自定义 Expectations 可以使测试代码更具表达力,更易于理解。
- 减少重复代码: 可以将常用的断言逻辑封装到自定义 Expectations 中,避免在多个测试用例中重复编写相同的代码。
- 提高测试效率: 通过自定义 Expectations,可以更快速地编写和维护测试用例。
使用 Data Providers 进行数据驱动测试
Pest 支持使用 Data Providers 来进行数据驱动测试。 Data Providers 允许我们使用不同的数据集运行相同的测试用例,从而更全面地验证代码的行为。
定义 Data Provider:
Data Provider 是一个返回数组的函数。数组中的每个元素都是一个数据集,将作为参数传递给测试用例。
示例:
<?php
function additionDataProvider(): array
{
return [
[1, 1, 2],
[2, 2, 4],
[3, 3, 6],
];
}
it('adds two numbers correctly', function (int $a, int $b, int $expected) {
expect($a + $b)->toBe($expected);
})->with('additionDataProvider');
代码解释:
additionDataProvider()函数定义了一个 Data Provider,它返回一个包含三个数据集的数组。it('adds two numbers correctly', function (int $a, int $b, int $expected) { ... })->with('additionDataProvider');定义了一个测试用例,并使用with()方法指定 Data Provider 为additionDataProvider。- Pest 将使用
additionDataProvider()返回的每个数据集作为参数调用测试用例。
Pest 与 Laravel 集成
Pest 可以与 Laravel 无缝集成,从而方便地测试 Laravel 应用程序。 Pest 提供了 Laravel 特定的测试助手,例如 get()、post()、assertDatabaseHas() 等。
示例:
<?php
use function PestLaravelget;
use function PestLaravelpost;
use function PestLaravelassertDatabaseHas;
it('creates a new post', function () {
post('/posts', [
'title' => 'My Post',
'content' => 'This is the content of my post.',
]);
assertDatabaseHas('posts', [
'title' => 'My Post',
]);
get('/posts')->assertSee('My Post');
});
代码解释:
use function PestLaravelget;引入 Pest 提供的 Laravel 测试助手get()。use function PestLaravelpost;引入 Pest 提供的 Laravel 测试助手post()。use function PestLaravelassertDatabaseHas;引入 Pest 提供的 Laravel 测试助手assertDatabaseHas()。post('/posts', [ ... ]);使用post()方法发起一个 POST 请求到/posts。assertDatabaseHas('posts', [ ... ]);使用assertDatabaseHas()方法断言数据库中存在满足指定条件的记录。get('/posts')->assertSee('My Post');使用get()方法发起一个 GET 请求到/posts,并使用assertSee()方法断言响应内容包含 "My Post"。
Pest 的插件生态
Pest 拥有丰富的插件生态,可以扩展其功能,例如:
- Pest Parallel: 用于并行执行测试用例,提高测试速度。
- Pest Coverage: 用于生成代码覆盖率报告。
- Pest Drifting: 用于自动重试失败的测试用例。
安装插件:
可以使用 Composer 安装 Pest 插件。
composer require pestphp/pest-plugin-parallel --dev
配置插件:
不同的插件有不同的配置方式,请参考插件的官方文档。
最佳实践
- 编写简洁、易读的测试用例: 使用 Pest 的简洁语法,使测试代码更易于理解和维护。
- 使用自定义 Expectations: 将常用的断言逻辑封装到自定义 Expectations 中,减少重复代码,提高代码可读性。
- 使用 Data Providers: 使用 Data Providers 进行数据驱动测试,更全面地验证代码的行为.
- 保持测试用例的独立性: 每个测试用例应该独立于其他测试用例,避免测试之间的相互影响。
- 编写全面的测试: 覆盖代码的所有重要功能和边界情况。
- 及时更新测试: 当代码发生变更时,及时更新测试用例。
总结与展望
Pest PHP 测试框架凭借其简洁的语法、强大的自定义能力和丰富的插件生态,为 PHP 开发者提供了一个优秀的测试解决方案。通过熟练掌握 Pest 的核心概念和最佳实践,可以编写更高效、更可靠的测试用例,从而提高代码质量和开发效率。未来,Pest 将继续发展壮大,为 PHP 测试领域带来更多创新。