Laravel 12 测试体系:Pest v3 集成与测试策略

摘要

本文详解 Laravel 12 与 Pest v3 的深度集成,包括测试用例的编写、测试套件的组织、Mock 与 Stub 的使用。提供完整的测试策略,覆盖单元测试、集成测试、功能测试和端到端测试,帮助开发者构建高质量、可维护的 Laravel 应用。

1. Laravel 测试体系概述

Laravel 12 提供了全面的测试支持,从单元测试到端到端测试,构建了一个完整的测试生态系统。

1.1 核心测试功能

  • PHPUnit 集成:内置 PHPUnit 支持
  • Pest v3 集成:官方推荐的测试框架
  • 测试辅助函数:丰富的测试辅助方法
  • 测试数据库:支持 SQLite 内存数据库
  • 浏览器测试:使用 Laravel Dusk 进行端到端测试
  • API 测试:专门的 API 测试辅助函数
  • Mock 与 Stub:内置的模拟功能
  • 测试覆盖率:支持代码覆盖率分析

1.2 测试类型与应用场景

测试类型描述适用场景工具
单元测试测试单个类或方法业务逻辑、工具类Pest/PHPUnit
集成测试测试多个组件的交互控制器、服务Pest/PHPUnit
功能测试测试完整的用户流程注册、登录、购物车Pest/PHPUnit
API 测试测试 API 端点RESTful API、GraphQLPest/PHPUnit
浏览器测试测试浏览器交互前端交互、JavaScriptLaravel Dusk

2. Pest v3 集成

Laravel 12 官方推荐使用 Pest v3 作为测试框架,提供了更简洁、更优雅的测试语法。

2.1 安装与配置

安装 Pest v3

1
2
3
4
5
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev

# 初始化 Pest
./vendor/bin/pest --init

配置 Pest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// tests/Pest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different test case class.
|
*/

uses(Tests\TestCase::class, RefreshDatabase::class, WithFaker::class)->in('Feature');
uses(Tests\TestCase::class)->in('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 out-of-the-box, you may have some testing code specific to your
t| project that you don't want to repeat in every test file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function actingAsAdmin()
{
return test()->actingAs(
App\Models\User::factory()->create(['role' => 'admin'])
);
}

2.2 Pest v3 语法特性

基本测试结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 测试示例
test('basic test', function () {
expect(true)->toBeTrue();
});

// 带描述的测试
test('user can register', function () {
// 测试代码
})->describe('User Registration');

// 带数据提供者的测试
test('addition works', function ($a, $b, $expected) {
expect($a + $b)->toBe($expected);
})->with([
[1, 2, 3],
[2, 3, 5],
[3, 4, 7],
]);

测试生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 测试套件设置
beforeEach(function () {
// 每个测试前执行
$this->user = User::factory()->create();
});

afterEach(function () {
// 每个测试后执行
// 清理代码
});

beforeAll(function () {
// 所有测试前执行一次
// 全局设置
});

afterAll(function () {
// 所有测试后执行一次
// 全局清理
});

测试分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 测试分组
describe('User Management', function () {
test('user can be created', function () {
// 测试代码
});

test('user can be updated', function () {
// 测试代码
});

test('user can be deleted', function () {
// 测试代码
});
});

// 嵌套分组
describe('Authentication', function () {
describe('Login', function () {
test('user can login with valid credentials', function () {
// 测试代码
});

test('user cannot login with invalid credentials', function () {
// 测试代码
});
});

describe('Registration', function () {
test('user can register with valid data', function () {
// 测试代码
});

test('user cannot register with invalid data', function () {
// 测试代码
});
});
});

3. 单元测试

单元测试是测试的基础,用于测试单个类或方法的功能。

3.1 编写单元测试

测试服务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// tests/Unit/Services/UserServiceTest.php
describe('UserService', function () {
test('can create user', function () {
// 准备
$userService = new UserService();
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

// 执行
$user = $userService->create($userData);

// 断言
expect($user)->toBeInstanceOf(User::class);
expect($user->name)->toBe('Test User');
expect($user->email)->toBe('test@example.com');
});

test('can find user by email', function () {
// 准备
$userService = new UserService();
$user = User::factory()->create(['email' => 'test@example.com']);

// 执行
$foundUser = $userService->findByEmail('test@example.com');

// 断言
expect($foundUser)->toBeInstanceOf(User::class);
expect($foundUser->id)->toBe($user->id);
});
});

测试工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// tests/Unit/Helpers/StrHelperTest.php
describe('StrHelper', function () {
test('can generate slug', function () {
// 执行
$slug = StrHelper::slug('Hello World');

// 断言
expect($slug)->toBe('hello-world');
});

test('can generate random string', function () {
// 执行
$random = StrHelper::random(10);

// 断言
expect($random)->toHaveLength(10);
});
});

3.2 Mock 与 Stub

使用 Mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
test('user service uses mailer to send welcome email', function () {
// 准备
$mailer = Mockery::mock(Mailer::class);
$mailer->shouldReceive('sendWelcomeEmail')
->once()
->with(Mockery::type(User::class));

$userService = new UserService($mailer);
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

// 执行
$user = $userService->create($userData);

// 断言
expect($user)->toBeInstanceOf(User::class);
});

使用 Laravel 的 Mock 辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test('user service uses mailer to send welcome email', function () {
// 准备
$this->mock(Mailer::class, function ($mock) {
$mock->shouldReceive('sendWelcomeEmail')
->once()
->with($this->callback(function ($user) {
return $user instanceof User;
}));
});

$userService = app(UserService::class);
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

// 执行
$user = $userService->create($userData);

// 断言
expect($user)->toBeInstanceOf(User::class);
});

4. 集成测试

集成测试用于测试多个组件的交互,如控制器与服务的协作。

4.1 测试控制器

基本控制器测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// tests/Feature/Controllers/UserControllerTest.php
describe('UserController', function () {
test('can get user profile', function () {
// 准备
$user = User::factory()->create();

// 执行
$response = $this->actingAs($user)->get('/profile');

// 断言
$response->assertStatus(200);
$response->assertViewIs('profile');
$response->assertViewHas('user', $user);
});

test('can update user profile', function () {
// 准备
$user = User::factory()->create();
$data = [
'name' => 'Updated Name',
'email' => 'updated@example.com',
];

// 执行
$response = $this->actingAs($user)->put('/profile', $data);

// 断言
$response->assertRedirect('/profile');
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
]);
});
});

表单验证测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test('cannot update profile with invalid data', function () {
// 准备
$user = User::factory()->create();
$data = [
'name' => '', // 空名称
'email' => 'invalid-email', // 无效邮箱
];

// 执行
$response = $this->actingAs($user)->put('/profile', $data);

// 断言
$response->assertStatus(302);
$response->assertSessionHasErrors(['name', 'email']);
$this->assertDatabaseMissing('users', [
'id' => $user->id,
'email' => 'invalid-email',
]);
});

4.2 测试服务

测试服务与数据库交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test('user service creates user in database', function () {
// 准备
$userService = app(UserService::class);
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

// 执行
$user = $userService->create($userData);

// 断言
expect($user)->toBeInstanceOf(User::class);
$this->assertDatabaseHas('users', [
'email' => 'test@example.com',
]);
});

测试服务与外部 API 交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test('payment service processes payment', function () {
// 准备
$this->mock(PaymentGateway::class, function ($mock) {
$mock->shouldReceive('charge')
->once()
->with(100, 'card_123')
->andReturn(['success' => true, 'transaction_id' => 'tx_123']);
});

$paymentService = app(PaymentService::class);
$user = User::factory()->create();

// 执行
$result = $paymentService->process($user, 100, 'card_123');

// 断言
expect($result)->toBeTrue();
$this->assertDatabaseHas('payments', [
'user_id' => $user->id,
'amount' => 100,
'transaction_id' => 'tx_123',
]);
});

5. API 测试

Laravel 12 提供了专门的 API 测试辅助函数,简化了 API 测试的编写。

5.1 基本 API 测试

测试 GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// tests/Feature/Api/UserApiTest.php
describe('User API', function () {
test('can get users list', function () {
// 准备
User::factory()->count(3)->create();
$user = User::factory()->create(['role' => 'admin']);

// 执行
$response = $this->actingAs($user)->getJson('/api/users');

// 断言
$response->assertStatus(200);
$response->assertJsonCount(4, 'data');
});

test('can get single user', function () {
// 准备
$user = User::factory()->create();
$admin = User::factory()->create(['role' => 'admin']);

// 执行
$response = $this->actingAs($admin)->getJson("/api/users/{$user->id}");

// 断言
$response->assertStatus(200);
$response->assertJson([
'data' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
],
]);
});
});

测试 POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
test('can create user', function () {
// 准备
$admin = User::factory()->create(['role' => 'admin']);
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'role' => 'user',
];

// 执行
$response = $this->actingAs($admin)->postJson('/api/users', $userData);

// 断言
$response->assertStatus(201);
$response->assertJson([
'data' => [
'name' => 'Test User',
'email' => 'test@example.com',
'role' => 'user',
],
]);
$this->assertDatabaseHas('users', [
'email' => 'test@example.com',
]);
});

5.2 API 认证测试

测试认证中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test('unauthorized user cannot access protected API', function () {
// 执行
$response = $this->getJson('/api/users');

// 断言
$response->assertStatus(401);
$response->assertJson([
'message' => 'Unauthenticated.',
]);
});

// test('user with invalid token cannot access protected API', function () {
// // 执行
// $response = $this->withHeaders([
// 'Authorization' => 'Bearer invalid-token',
// ])->getJson('/api/users');
//
// // 断言
// $response->assertStatus(401);
// $response->assertJson([
// 'message' => 'Unauthenticated.',
// ]);
// });

测试 API 速率限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test('API has rate limit', function () {
// 准备
$user = User::factory()->create();

// 执行多次请求
for ($i = 0; $i < 60; $i++) {
$response = $this->actingAs($user)->getJson('/api/users');
}

// 断言
$response->assertStatus(429);
$response->assertJson([
'message' => 'Too Many Attempts.',
]);
});

6. 浏览器测试

Laravel Dusk 提供了强大的浏览器测试功能,可以测试前端交互和 JavaScript 功能。

6.1 安装与配置

安装 Laravel Dusk

1
2
3
4
composer require laravel/dusk --dev

# 安装 Dusk
php artisan dusk:install

配置 Dusk

1
2
3
4
5
6
7
// .env.dusk.local
APP_URL=http://localhost:8000
APP_ENV=testing
APP_DEBUG=true

DB_CONNECTION=sqlite
DB_DATABASE=:memory:

6.2 编写浏览器测试

测试登录流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// tests/Browser/LoginTest.php
namespace Tests\Browser;

use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class LoginTest extends DuskTestCase
{
test('user can login', function () {
// 准备
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

// 执行
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'password123')
->press('Login')
->assertPathIs('/dashboard')
->assertSee('Dashboard');
});
});

test('user cannot login with invalid credentials', function () {
// 执行
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('email', 'invalid@example.com')
->type('password', 'wrong-password')
->press('Login')
->assertPathIs('/login')
->assertSee('These credentials do not match our records.');
});
});
}

测试表单提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test('user can register', function () {
// 执行
$this->browse(function (Browser $browser) {
$browser->visit('/register')
->type('name', 'Test User')
->type('email', 'test@example.com')
->type('password', 'password123')
->type('password_confirmation', 'password123')
->press('Register')
->assertPathIs('/dashboard')
->assertSee('Dashboard');
});

// 断言
$this->assertDatabaseHas('users', [
'email' => 'test@example.com',
]);
});

7. 测试套件的组织

7.1 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
tests/
├── Unit/ # 单元测试
│ ├── Services/ # 服务测试
│ ├── Helpers/ # 辅助函数测试
│ └── Models/ # 模型测试
├── Feature/ # 集成测试
│ ├── Controllers/ # 控制器测试
│ ├── Api/ # API 测试
│ └── Pages/ # 页面测试
├── Browser/ # 浏览器测试
├── Pest.php # Pest 配置
└── TestCase.php # 基础测试类

7.2 测试分类

按功能分类

1
2
3
4
5
6
tests/
├── Feature/
│ ├── Authentication/ # 认证相关测试
│ ├── UserManagement/ # 用户管理测试
│ ├── Payment/ # 支付相关测试
│ └── Product/ # 产品相关测试

按模块分类

1
2
3
4
5
tests/
├── Feature/
│ ├── Admin/ # 管理员功能测试
│ ├── User/ # 用户功能测试
│ └── Guest/ # 访客功能测试

8. 测试覆盖率分析

8.1 配置代码覆盖率

安装 Xdebug

1
2
3
4
5
# 安装 Xdebug
sudo apt-get install php-xdebug

# 或使用 PECL
pecl install xdebug

配置 PHPUnit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- phpunit.xml -->
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory suffix=".php">./app/Http/Controllers</directory>
<directory suffix=".php">./app/Models</directory>
</exclude>
</coverage>
<!-- ... -->
</phpunit>

8.2 生成覆盖率报告

使用 Pest 生成覆盖率报告

1
2
3
4
5
6
7
8
# 生成 HTML 覆盖率报告
./vendor/bin/pest --coverage-html=coverage

# 生成 XML 覆盖率报告
./vendor/bin/pest --coverage-clover=coverage.xml

# 生成文本覆盖率报告
./vendor/bin/pest --coverage-text

分析覆盖率报告

覆盖率报告通常包括以下指标:

  • 行覆盖率:测试覆盖的代码行数百分比
  • 分支覆盖率:测试覆盖的代码分支百分比
  • 路径覆盖率:测试覆盖的代码路径百分比
  • 函数覆盖率:测试覆盖的函数百分比

8.3 提高测试覆盖率

识别未覆盖的代码

1
2
# 显示未覆盖的代码
./vendor/bin/pest --coverage-text --coverage-show-uncovered

优先测试核心功能

  1. 业务逻辑:优先测试核心业务逻辑
  2. 边界情况:测试边界条件和异常情况
  3. 关键路径:测试用户常用的功能路径
  4. 易错代码:测试历史上容易出错的代码

9. CI/CD 中的测试自动化

9.1 GitHub Actions 配置

基本配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# .github/workflows/run-tests.yml
name: Run Tests

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, dom, curl, sqlite3
coverage: xdebug

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run tests
run: ./vendor/bin/pest --coverage-clover=coverage.xml

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

多环境测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# .github/workflows/run-tests.yml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.1, 8.2, 8.3]
laravel: [10.*, 11.*, 12.*]

steps:
# ...

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" --no-update
composer install --prefer-dist --no-progress

- name: Run tests
run: ./vendor/bin/pest

9.2 GitLab CI 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# .gitlab-ci.yml
stages:
- test

run_tests:
stage: test
image: php:8.2
script:
- apt-get update && apt-get install -y git unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress
- ./vendor/bin/pest
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml

10. 测试最佳实践

10.1 编写高质量测试

  • 测试名称:使用描述性的测试名称
  • 测试结构:遵循 Arrange-Act-Assert 模式
  • 测试隔离:每个测试应该独立运行
  • 测试速度:保持测试快速运行
  • 测试维护:保持测试代码的整洁和可维护

10.2 测试原则

  • FIRST 原则

    • Fast:测试应该快速运行
    • Independent:测试应该相互独立
    • Repeatable:测试应该可重复运行
    • Self-validating:测试应该自动验证结果
    • Timely:测试应该及时编写
  • 测试金字塔

    • 底部:大量的单元测试
    • 中间:适量的集成测试
    • 顶部:少量的端到端测试

10.3 常见测试陷阱

  • 测试实现细节:避免测试实现细节,应该测试行为
  • 过度模拟:不要过度使用 Mock,应该测试真实的交互
  • 测试环境依赖:避免测试依赖外部服务
  • 测试速度慢:避免测试中使用真实的数据库和网络请求
  • 测试覆盖率迷恋:不要只追求覆盖率,应该关注测试质量

11. 实战案例:构建完整的测试套件

11.1 项目背景

  • 规模:中型 e-commerce 应用
  • 功能:产品管理、用户管理、购物车、支付
  • 要求:测试覆盖率 > 80%

11.2 测试策略设计

1. 测试分层

  • 单元测试:70%

    • 服务层测试
    • 工具类测试
    • 模型测试
  • 集成测试:20%

    • 控制器测试
    • API 测试
    • 服务集成测试
  • 端到端测试:10%

    • 关键用户流程
    • 支付流程
    • 管理员功能

2. 测试重点

  • 核心业务逻辑:产品管理、购物车、支付
  • 用户流程:注册、登录、购买
  • API 接口:RESTful API
  • 边界情况:库存不足、支付失败

3. 测试实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 产品服务测试
describe('ProductService', function () {
test('can create product', function () {
// 测试代码
});

test('can update product', function () {
// 测试代码
});

test('can delete product', function () {
// 测试代码
});

test('can get product by id', function () {
// 测试代码
});

test('can search products', function () {
// 测试代码
});
});

// 购物车服务测试
describe('CartService', function () {
test('can add product to cart', function () {
// 测试代码
});

test('can remove product from cart', function () {
// 测试代码
});

test('can update cart item quantity', function () {
// 测试代码
});

test('can calculate cart total', function () {
// 测试代码
});
});

// 支付服务测试
describe('PaymentService', function () {
test('can process payment', function () {
// 测试代码
});

test('can refund payment', function () {
// 测试代码
});

test('handles payment failure', function () {
// 测试代码
});
});

11.3 测试效果

指标实现前实现后提升
测试覆盖率0%85%85%
代码质量中等显著
缺陷率10%2%80%
开发速度显著
维护成本显著

12. 总结

Laravel 12 的测试体系提供了从单元测试到端到端测试的完整支持,结合 Pest v3 的优雅语法,为开发者构建高质量应用提供了强大的工具。

通过本文的介绍,开发者可以快速掌握 Laravel 12 的测试能力,构建一个完整、高效的测试套件。测试不仅可以保证代码质量,还可以提高开发效率,减少生产环境的 bug。

在实际项目中,应该根据项目的规模和复杂度,选择合适的测试策略,平衡测试的深度和广度。同时,应该将测试作为开发流程的一部分,持续编写和维护测试代码。

随着 Laravel 生态系统的不断发展,测试工具和实践也在不断演进。开发者应该保持关注 Laravel 的更新,及时采用新的测试技术和工具,为构建更好的 Laravel 应用而努力。