Pest PHP测试框架:简洁语法与自定义断言(Expectations)的实践应用

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:

  1. 创建 Expectation 类: 创建一个新的类,并继承自 PestExpectation
  2. 定义断言方法: 在类中定义新的断言方法。
  3. 注册 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 测试领域带来更多创新。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注