Pest PHP:优雅语法驱动的测试开发之旅
大家好,今天我们一起来聊聊如何利用 Pest PHP 进行测试驱动开发 (TDD)。Pest PHP 并不是一个全新的测试框架,它构建于 PHPUnit 之上,提供了一种更为简洁、优雅的语法,让我们能够更专注于测试逻辑本身,而不是被繁琐的配置和语法所困扰。
TDD 的核心理念
在深入 Pest PHP 之前,我们需要再次强调 TDD 的核心理念:
- 编写失败的测试 (Red): 在编写任何实际代码之前,先编写一个描述期望行为的测试用例,并且这个测试用例必然会失败。
- 编写最少代码使其通过 (Green): 编写刚好能让测试用例通过的最少量代码。
- 重构 (Refactor): 清理代码,消除重复,提高可读性和可维护性,同时确保所有测试仍然通过。
这个循环不断重复,直到实现所有功能。
Pest PHP 的优势
- 简洁的语法: Pest 提供了更易读、更易写的语法,减少了样板代码,使测试更清晰。
- 强大的断言: Pest 继承了 PHPUnit 的所有断言方法,并添加了一些额外的实用断言。
- 并行测试: Pest 支持并行运行测试,显著缩短测试时间。
- 扩展性: Pest 允许你创建自定义的测试套件、帮助函数和插件,以满足特定的项目需求。
- 内置的代码覆盖率报告: Pest 可以生成代码覆盖率报告,帮助你了解测试的覆盖程度。
Pest PHP 的安装与配置
首先,确保你已经安装了 Composer。接下来,通过 Composer 安装 Pest:
composer require pestphp/pest --dev
安装完成后,初始化 Pest:
./vendor/bin/pest --init
这会在你的项目根目录下创建 tests 目录,以及 Pest.php 文件,用于配置 Pest。Pest.php 文件通常包含以下内容:
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to define your test case is being bound to the application
| instance. This allows you to use any of this classes' methods while
| testing.
|
*/
uses(TestsTestCase::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. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful, you may need to add some helpers that you
| can access from your tests. Here you can register them.
|
*/
uses() 函数定义了哪些测试文件应该使用哪个 TestCase 类。expect()->extend() 允许你定义自定义的断言。
TDD 示例:一个简单的计算器类
让我们通过一个简单的计算器类的例子来演示如何使用 Pest 进行 TDD。
步骤 1: 编写失败的测试 (Red)
首先,我们创建一个 Calculator 类,但是暂时不实现任何功能。然后在 tests/Unit 目录下创建一个 CalculatorTest.php 文件,编写一个测试用例,测试加法功能。
<?php
use AppCalculator;
it('can add two numbers', function () {
$calculator = new Calculator();
expect($calculator->add(2, 3))->toBe(5);
});
这个测试用例会失败,因为 Calculator 类还没有 add 方法。运行测试:
./vendor/bin/pest
你会看到一个失败的测试报告。
步骤 2: 编写最少代码使其通过 (Green)
现在,我们编写 Calculator 类的 add 方法,使其通过测试。
<?php
namespace App;
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
}
再次运行测试:
./vendor/bin/pest
这次,测试应该通过了。
步骤 3: 重构 (Refactor)
目前的代码非常简单,不需要重构。
继续添加功能:减法
接下来,我们添加减法功能。
步骤 1: 编写失败的测试 (Red)
<?php
use AppCalculator;
it('can add two numbers', function () {
$calculator = new Calculator();
expect($calculator->add(2, 3))->toBe(5);
});
it('can subtract two numbers', function () {
$calculator = new Calculator();
expect($calculator->subtract(5, 2))->toBe(3);
});
运行测试,你会看到减法测试失败。
步骤 2: 编写最少代码使其通过 (Green)
<?php
namespace App;
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
public function subtract(int $a, int $b): int
{
return $a - $b;
}
}
再次运行测试,所有测试应该都通过了。
步骤 3: 重构 (Refactor)
目前的代码仍然很简单,不需要重构。
更复杂的示例:除法,并处理除以零的情况
接下来,我们添加除法功能,并且需要处理除以零的情况。
步骤 1: 编写失败的测试 (Red)
<?php
use AppCalculator;
it('can add two numbers', function () {
$calculator = new Calculator();
expect($calculator->add(2, 3))->toBe(5);
});
it('can subtract two numbers', function () {
$calculator = new Calculator();
expect($calculator->subtract(5, 2))->toBe(3);
});
it('can divide two numbers', function () {
$calculator = new Calculator();
expect($calculator->divide(10, 2))->toBe(5);
});
it('throws an exception when dividing by zero', function () {
$calculator = new Calculator();
$calculator->divide(10, 0);
})->throws(InvalidArgumentException::class, 'Cannot divide by zero');
注意,我们使用了 ->throws() 方法来断言除以零时会抛出一个 InvalidArgumentException 异常,并带有特定的错误消息。我们需要先引入 InvalidArgumentException 类。
<?php
use AppCalculator;
use InvalidArgumentException;
it('can add two numbers', function () {
$calculator = new Calculator();
expect($calculator->add(2, 3))->toBe(5);
});
it('can subtract two numbers', function () {
$calculator = new Calculator();
expect($calculator->subtract(5, 2))->toBe(3);
});
it('can divide two numbers', function () {
$calculator = new Calculator();
expect($calculator->divide(10, 2))->toBe(5);
});
it('throws an exception when dividing by zero', function () {
$calculator = new Calculator();
$calculator->divide(10, 0);
})->throws(InvalidArgumentException::class, 'Cannot divide by zero');
运行测试,你会看到除法测试和异常测试都失败。
步骤 2: 编写最少代码使其通过 (Green)
<?php
namespace App;
use InvalidArgumentException;
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
public function subtract(int $a, int $b): int
{
return $a - $b;
}
public function divide(int $a, int $b): float
{
if ($b === 0) {
throw new InvalidArgumentException('Cannot divide by zero');
}
return $a / $b;
}
}
再次运行测试,所有测试应该都通过了。
步骤 3: 重构 (Refactor)
目前的代码仍然很简单,不需要重构。
Pest PHP 的高级特性
-
数据提供器: 可以使用数据提供器来运行相同的测试用例,但使用不同的数据。
<?php use AppCalculator; it('can add two numbers', function (int $a, int $b, int $expected) { $calculator = new Calculator(); expect($calculator->add($a, $b))->toBe($expected); })->with([ [2, 3, 5], [5, 5, 10], [0, 0, 0], ]); -
分组: 可以将测试用例分组,以便更有组织地运行测试。
<?php uses()->group('calculator'); it('can add two numbers', function () { // ... }); it('can subtract two numbers', function () { // ... });然后可以使用
--group选项运行特定组的测试:./vendor/bin/pest --group calculator -
并发测试: 可以使用
--parallel选项并行运行测试,显著缩短测试时间。./vendor/bin/pest --parallel -
自定义断言: 如
Pest.php文件中所示,可以使用expect()->extend()来创建自定义断言,使测试更具可读性。
一些最佳实践
- 编写清晰、简洁的测试用例: 测试用例应该易于理解,并且只测试一个特定的行为。
- 使用有意义的测试名称: 测试名称应该清楚地描述被测试的行为。
- 保持测试独立: 每个测试用例应该独立运行,不依赖于其他测试用例。
- 定期运行测试: 在每次代码更改后都应该运行测试,以确保没有引入新的错误。
- 保持代码覆盖率较高: 目标是尽可能覆盖所有的代码路径,以确保代码的质量。
Pest 语法与 PHPUnit 的对比
| 特性 | Pest PHP | PHPUnit |
|---|---|---|
| 测试用例定义 | it('description', function () { // test logic }); |
public function testDescription() { // test logic } |
| 断言 | expect($value)->toBe($expected); |
$this->assertEquals($expected, $value); |
| 异常断言 | $this->expectException(Exception::class); |
$this->expectException(Exception::class); $this->expectExceptionMessage('message'); |
| 数据提供器 | ->with([...]); |
@dataProvider providerName |
| Setup/Teardown | beforeEach(function () { // setup }); |
protected function setUp(): void { // setup } protected function tearDown(): void { // teardown } |
架构设计与测试
TDD 不仅仅是编写测试,它还能影响你的架构设计。通过先编写测试,你会更清楚地了解你的代码需要做什么,以及如何将代码组织成更小的、更易于测试的模块。这可以帮助你避免创建庞大的、难以维护的代码库。
例如,如果你正在开发一个 Web API,你可以先编写测试来定义 API 的端点、请求和响应。这可以帮助你设计一个清晰、一致的 API。
结合领域驱动设计 (DDD)
TDD 与 DDD 相辅相成。DDD 强调将业务逻辑封装到领域模型中,而 TDD 可以帮助你验证这些领域模型的行为是否符合预期。
例如,如果你正在开发一个电子商务应用程序,你可以创建一个 Order 领域模型,并使用 TDD 来验证订单的创建、更新和取消等操作是否正确。
持续集成/持续部署 (CI/CD)
将 TDD 纳入 CI/CD 流程可以确保代码在部署到生产环境之前经过充分的测试。这可以帮助你减少错误,提高代码质量。
在 CI/CD 流程中,每次代码提交都会触发自动化构建和测试过程。如果任何测试失败,构建就会失败,阻止代码部署到生产环境。
结论
Pest PHP 提供了一种优雅、简洁的方式来进行测试驱动开发。通过遵循 TDD 的原则,你可以编写更高质量、更易于维护的代码,并改善你的架构设计。 记住,TDD 是一个迭代的过程,需要不断地练习和实践才能掌握。希望今天的分享能够帮助你开始你的 Pest PHP 之旅。
优雅语法的确可以提升开发效率,但更重要的是理解TDD的本质。
测试驱动开发,先测后码,保障代码质量,辅助架构设计。
选择合适的工具只是第一步,关键在于坚持TDD的原则并将其融入开发流程。