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

摘要

本文深入剖析 Laravel 12 与 Pest v3 的深度集成,从测试架构设计原理到具体实现细节,全面覆盖测试用例编写、测试套件组织、Mock 与 Stub 使用、测试覆盖率分析等核心领域。提供完整的测试策略体系,涵盖单元测试、集成测试、功能测试、API 测试和端到端测试,结合实际项目场景,帮助开发者构建高质量、可维护的 Laravel 应用。通过专业的测试技术和最佳实践,确保应用在各种场景下的可靠性和稳定性,为持续集成和持续部署提供坚实的质量保障。

1. Laravel 测试体系概述

Laravel 12 构建了一个完整、分层的测试生态系统,基于 PHPUnit 和 Pest v3,从单元测试到端到端测试,提供了全面的工具和辅助函数,帮助开发者构建高质量、可维护的应用。Laravel 的测试体系设计遵循测试金字塔原则,强调从底层到顶层的全面覆盖,确保应用在各种场景下的可靠性和稳定性。

1.1 核心测试功能

  • PHPUnit 集成:内置 PHPUnit 支持,作为底层测试框架,提供完整的断言和测试运行器
  • Pest v3 集成:官方推荐的测试框架,基于 PHPUnit 构建,提供更简洁、更优雅的测试语法
  • 测试辅助函数:丰富的测试辅助方法,简化测试代码编写,如 actingAs()get()post()
  • 测试数据库:支持 SQLite 内存数据库、MySQL、PostgreSQL 等多种测试数据库,自动管理数据库连接
  • 事务性测试:自动回滚数据库操作,确保测试隔离,避免测试之间的相互影响
  • 浏览器测试:使用 Laravel Dusk 进行端到端测试,支持 JavaScript 交互和浏览器自动化
  • API 测试:专门的 API 测试辅助函数,简化 HTTP 请求和响应断言,支持 JSON 和 XML 响应
  • Mock 与 Stub:内置的模拟功能,支持依赖注入的测试,包括 Facade 模拟和服务容器模拟
  • 测试覆盖率:支持代码覆盖率分析,帮助识别未测试的代码,集成 Xdebug 和 PHP_CodeCoverage
  • 测试环境配置:独立的测试环境配置,避免影响开发和生产环境,支持环境变量和配置文件
  • 并行测试:支持并行运行测试,显著提高测试执行速度,特别是在大型项目中
  • 测试套件组织:灵活的测试套件组织方式,支持按功能、模块或测试类型分组
  • 测试数据生成:内置的模型工厂和测试数据生成器,简化测试数据的创建和管理

1.2 测试类型与应用场景

测试类型描述适用场景工具执行速度维护成本最佳实践
单元测试测试单个类或方法的功能,隔离外部依赖业务逻辑、工具类、服务、模型方法Pest/PHPUnit极快 (毫秒级)使用 Mock/Stub 隔离依赖,测试边界情况和异常
集成测试测试多个组件的交互,验证协作正确性控制器、服务、数据库、队列、邮件Pest/PHPUnit + 测试数据库中等 (秒级)使用事务性测试,避免测试数据污染
功能测试测试完整的用户流程,模拟用户交互注册、登录、购物车、支付、订单流程Pest/PHPUnit + 测试浏览器中等 (秒级)测试核心用户旅程,避免测试实现细节
API 测试测试 API 端点的响应,验证数据格式和状态码RESTful API、GraphQL、WebSocketPest/PHPUnit + API 测试助手快 (毫秒级)测试成功和失败场景,验证响应结构和 headers
浏览器测试测试浏览器交互和 JavaScript,端到端验证前端交互、表单验证、SPA、复杂 UI 流程Laravel Dusk + ChromeDriver慢 (秒级/分钟级)专注于核心用户流程,使用页面对象模式
性能测试测试应用的性能和响应时间,识别瓶颈API 响应、数据库查询、页面加载、并发处理Laravel Benchmark + 压测工具中等 (秒级/分钟级)建立性能基线,监控关键指标变化
安全测试测试应用的安全性,识别漏洞认证、授权、输入验证、SQL 注入、XSSPest/PHPUnit + 安全工具中等 (秒级)集成安全扫描工具,定期运行安全测试
冒烟测试测试应用的基本功能,确保关键路径可用部署验证、主要功能检查Pest/PHPUnit快 (秒级)包含核心功能测试,作为部署前检查
回归测试测试应用的现有功能,确保修改不破坏现有功能代码修改、依赖更新、重构Pest/PHPUnit中等 (秒级/分钟级)自动化运行,与 CI/CD 集成

1.3 测试金字塔策略

Laravel 推荐采用测试金字塔策略,构建分层的测试体系,确保测试的有效性和效率:

  1. 底层(基础):大量的单元测试(70-80%)

    • 测试目标:单个组件的功能,如服务、工具类、模型方法等
    • 技术特点:隔离外部依赖,使用 Mock/Stub,执行速度快(毫秒级)
    • 价值:提供快速反馈,捕获大多数逻辑错误,维护成本低
    • 最佳实践:测试边界情况、异常处理、核心业务逻辑
  2. 中层(桥梁):适量的集成测试和 API 测试(15-20%)

    • 测试目标:组件之间的交互,如控制器与服务、服务与数据库等
    • 技术特点:验证协作正确性,使用事务性测试,执行速度中等(秒级)
    • 价值:确保系统各部分协同工作,验证业务流程的正确性
    • 最佳实践:测试完整的业务流程,验证数据库操作,测试 API 响应
  3. 顶层(验证):少量的浏览器测试(5-10%)

    • 测试目标:完整的用户流程,如注册、登录、购物车等
    • 技术特点:端到端验证,支持 JavaScript 交互,执行速度慢(秒级/分钟级)
    • 价值:验证前端和后端的集成,确保关键用户场景的正确性
    • 最佳实践:专注于核心用户流程,使用页面对象模式,避免测试实现细节

测试金字塔的优势

  • 快速反馈:底层测试执行速度快,能快速发现问题
  • 成本效益:底层测试维护成本低,上层测试维护成本高
  • 全面覆盖:从单元到端到端的全面测试覆盖
  • 可靠性:多层测试确保系统的可靠性和稳定性

实施建议

  • 根据项目规模和复杂度调整测试比例
  • 优先编写单元测试,确保核心逻辑的正确性
  • 为关键业务流程编写集成测试
  • 为核心用户旅程编写浏览器测试
  • 集成测试覆盖率工具,监控测试覆盖情况

1.4 测试环境配置

基础配置

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 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"
stopOnFailure="false"
executionOrder="random"
resolveDependencies="true"
processIsolation="false"
printerClass="PHPUnit\TextUI\ResultPrinter"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
<testsuite name="Browser">
<directory suffix="Test.php">tests/Browser</directory>
</testsuite>
<testsuite name="Api">
<directory suffix="Test.php">tests/Api</directory>
</testsuite>
<testsuite name="Integration">
<directory suffix="Test.php">tests/Integration</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true" includeUncoveredFiles="true">
<include>
<directory suffix=".php">app</directory>
</include>
<exclude>
<directory suffix=".php">app/Http/Controllers</directory>
<directory suffix=".php">app/Models</directory>
<directory suffix=".php">app/Console</directory>
<directory suffix=".php">app/Exceptions</directory>
<directory suffix=".php">app/Providers</directory>
<directory suffix=".php">app/Filament</directory>
<directory suffix=".php">app/Livewire</directory>
</exclude>
<report>
<html outputDirectory="coverage/html" lowUpperBound="35" highLowerBound="70"/>
<xml outputDirectory="coverage/xml"/>
<text outputFile="coverage/text.txt" showUncoveredFiles="true"/>
</report>
</coverage>
<php>
<!-- 基础环境配置 -->
<env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="true"/>
<env name="APP_KEY" value="base64:your-testing-app-key"/>

<!-- 性能优化 -->
<env name="BCRYPT_ROUNDS" value="4"/>

<!-- 缓存配置 -->
<env name="CACHE_DRIVER" value="array"/>
<env name="FILESYSTEM_DISK" value="local"/>

<!-- 数据库配置 -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<!-- 如需使用 MySQL 或 PostgreSQL,请取消注释以下配置 -->
<!-- <env name="DB_CONNECTION" value="mysql"/> -->
<!-- <env name="DB_HOST" value="127.0.0.1"/> -->
<!-- <env name="DB_PORT" value="3306"/> -->
<!-- <env name="DB_DATABASE" value="laravel_test"/> -->
<!-- <env name="DB_USERNAME" value="root"/> -->
<!-- <env name="DB_PASSWORD" value=""/> -->

<!-- 邮件配置 -->
<env name="MAIL_MAILER" value="array"/>

<!-- 队列配置 -->
<env name="QUEUE_CONNECTION" value="sync"/>

<!-- 会话配置 -->
<env name="SESSION_DRIVER" value="array"/>
<env name="SESSION_LIFETIME" value="120"/>

<!-- 调试工具配置 -->
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="FORTIFY_ENABLED" value="true"/>

<!-- 日志配置 -->
<env name="LOG_CHANNEL" value="stderr"/>
<env name="LOG_LEVEL" value="error"/>

<!-- 第三方服务配置 -->
<env name="REDIS_HOST" value="127.0.0.1"/>
<env name="REDIS_PASSWORD" value=""/>
<env name="REDIS_PORT" value="6379"/>

<!-- API 配置 -->
<env name="API_PREFIX" value="api"/>
<env name="API_VERSION" value="v1"/>
</php>
</phpunit>

测试环境变量

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# .env.testing
# 应用配置
APP_NAME="Laravel Testing"
APP_ENV=testing
APP_KEY=base64:your-testing-app-key
APP_DEBUG=true
APP_URL=http://localhost

# 日志配置
LOG_CHANNEL=stderr
LOG_LEVEL=error
LOG_STACK=single

# 数据库配置
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
# 如需使用 MySQL 或 PostgreSQL,请取消注释以下配置
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel_test
# DB_USERNAME=root
# DB_PASSWORD=

# 缓存配置
CACHE_DRIVER=array
FILESYSTEM_DISK=local

# 队列配置
QUEUE_CONNECTION=sync
QUEUE_RETRY_AFTER=90

# 会话配置
SESSION_DRIVER=array
SESSION_LIFETIME=120

# 邮件配置
MAIL_MAILER=array
MAIL_FROM_ADDRESS="test@example.com"
MAIL_FROM_NAME="${APP_NAME}"

# Redis 配置
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=phpredis

# Memcached 配置
MEMCACHED_HOST=127.0.0.1
MEMCACHED_PORT=11211

# AWS 配置
AWS_ACCESS_KEY_ID=testing
AWS_SECRET_ACCESS_KEY=testing
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=testing
AWS_USE_PATH_STYLE_ENDPOINT=false

# Pusher 配置
PUSHER_APP_ID=testing
PUSHER_APP_KEY=testing
PUSHER_APP_SECRET=testing
PUSHER_APP_CLUSTER=mt1
PUSHER_SCHEME=http

# API 配置
API_PREFIX=api
API_VERSION=v1
API_DEBUG=true

# 测试特定配置
TESTING_SEED=true
TESTING_FACTORY_RESET=false
TESTING_PARALLEL=true
TESTING_TIMEOUT=30

# 第三方服务配置
STRIPE_KEY=sk_test_your-testing-key
STRIPE_SECRET=sk_test_your-testing-secret
PAYPAL_CLIENT_ID=your-testing-client-id
PAYPAL_CLIENT_SECRET=your-testing-client-secret

# 前端配置
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_APP_URL="${APP_URL}"
MIX_API_PREFIX="${API_PREFIX}"
MIX_API_VERSION="${API_VERSION}"

2. Pest v3 集成

Laravel 12 官方推荐使用 Pest v3 作为测试框架,基于 PHPUnit 构建,提供了更简洁、更优雅的测试语法,同时保留了 PHPUnit 的全部功能。Pest v3 引入了许多新特性,如更强大的数据提供者、更灵活的测试分组、更好的类型提示支持等。

2.1 安装与配置

安装 Pest v3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 安装核心包
composer require pestphp/pest --dev

# 安装 Laravel 插件
composer require pestphp/pest-plugin-laravel --dev

# 安装代码覆盖率插件
composer require pestphp/pest-plugin-coverage --dev

# 安装并行测试插件
composer require pestphp/pest-plugin-parallel --dev

# 安装类型提示插件
composer require pestphp/pest-plugin-type-coverage --dev

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

# 验证安装
./vendor/bin/pest --version

配置 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 高级语法特性

测试分组与描述

Pest v3 提供了强大的测试分组和描述功能,帮助你组织测试代码并提高可读性。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 基本测试分组
describe('User Management', function () {
// 测试前准备
beforeEach(function () {
$this->userService = app(UserService::class);
});

test('can create user', function () {
// 准备
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

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

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

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

// 执行
$updatedUser = $this->userService->update($user->id, $updateData);

// 断言
expect($updatedUser)->toBeInstanceOf(User::class);
expect($updatedUser->name)->toBe('Updated Name');
expect($updatedUser->email)->toBe('updated@example.com');
});

test('can delete user', function () {
// 准备
$user = User::factory()->create();

// 执行
$result = $this->userService->delete($user->id);

// 断言
expect($result)->toBeTrue();
expect(User::find($user->id))->toBeNull();
});
});

// 嵌套分组
describe('Authentication', function () {
// 共享的测试前准备
beforeEach(function () {
$this->authService = app(AuthService::class);
});

describe('Login', function () {
test('user can login with valid credentials', function () {
// 准备
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

// 执行
$result = $this->authService->login([
'email' => 'test@example.com',
'password' => 'password123',
]);

// 断言
expect($result)->toHaveKey('token');
expect($result)->toHaveKey('user');
expect($result['user']['email'])->toBe('test@example.com');
});

test('user cannot login with invalid credentials', function () {
// 准备
User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

// 执行 & 断言
$this->expectException(InvalidCredentialsException::class);
$this->authService->login([
'email' => 'test@example.com',
'password' => 'wrong-password',
]);
});

test('user cannot login with locked account', function () {
// 准备
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
'locked_at' => now(),
]);

// 执行 & 断言
$this->expectException(LockedAccountException::class);
$this->authService->login([
'email' => 'test@example.com',
'password' => 'password123',
]);
});
});

describe('Registration', function () {
test('user can register with valid data', function () {
// 准备
$registrationData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
];

// 执行
$user = $this->authService->register($registrationData);

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

test('user cannot register with existing email', function () {
// 准备
User::factory()->create(['email' => 'existing@example.com']);

$registrationData = [
'name' => 'Test User',
'email' => 'existing@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
];

// 执行 & 断言
$this->expectException(EmailAlreadyExistsException::class);
$this->authService->register($registrationData);
});
});
});

// 使用 context 进行场景描述
describe('Product Management', function () {
context('when product is in stock', function () {
test('can add to cart', function () {
// 测试代码
});

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

context('when product is out of stock', function () {
test('cannot add to cart', function () {
// 测试代码
});

test('shows out of stock message', function () {
// 测试代码
});
});
});

数据提供者

Pest v3 提供了强大的数据提供者功能,允许你使用不同的数据集运行同一个测试,提高测试覆盖率和代码复用性。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 基本数据提供者
test('addition works', function ($a, $b, $expected) {
expect($a + $b)->toBe($expected);
})->with([
[1, 2, 3],
[2, 3, 5],
[3, 4, 7],
[10, 20, 30],
[-1, 1, 0],
[0, 0, 0],
]);

// 命名数据提供者 - 使用关联数组提高可读性
test('user validation rules', function ($field, $value, $shouldPass, $description) {
// 准备
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
$field => $value,
];

// 执行
$validator = Validator::make($userData, [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);

// 断言
expect($validator->passes())->toBe($shouldPass, $description);
})->with([
['name', 'Test User', true, 'Valid name should pass'],
['name', '', false, 'Empty name should fail'],
['name', str_repeat('a', 256), false, 'Name longer than 255 chars should fail'],
['email', 'test@example.com', true, 'Valid email should pass'],
['email', 'invalid-email', false, 'Invalid email format should fail'],
['email', '', false, 'Empty email should fail'],
['password', 'password123', true, 'Valid password should pass'],
['password', 'pass', false, 'Password shorter than 8 chars should fail'],
['password', '', false, 'Empty password should fail'],
]);

// 动态数据提供者 - 函数返回数据
test('product price calculation', function ($quantity, $price, $expected) {
// 准备
$product = createTestProduct(['price' => $price]);

// 执行
$total = $product->calculateTotal($quantity);

// 断言
expect($total)->toBe($expected);
})->with(function () {
return [
[1, 100, 100],
[2, 100, 200],
[5, 100, 500],
[10, 100, 1000],
[0, 100, 0],
[1, 0, 0],
];
});

// 动态数据提供者 - 使用模型工厂
test('order total calculation', function ($items, $expectedTotal) {
// 准备
$order = createTestOrder();

// 添加商品到订单
foreach ($items as $itemData) {
$product = createTestProduct(['price' => $itemData['price']]);
$order->items()->create([
'product_id' => $product->id,
'quantity' => $itemData['quantity'],
'price' => $product->price,
]);
}

// 执行
$calculatedTotal = $order->calculateTotal();

// 断言
expect($calculatedTotal)->toBe($expectedTotal);
})->with(function () {
return [
'single item' => [[['quantity' => 1, 'price' => 100]], 100],
'multiple items' => [[['quantity' => 2, 'price' => 50], ['quantity' => 1, 'price' => 100]], 200],
'zero quantity' => [[['quantity' => 0, 'price' => 100]], 0],
'empty order' => [[], 0],
];
});

// 外部数据提供者 - 从文件或数据库获取数据
test('shipping cost calculation', function ($weight, $destination, $expectedCost) {
// 准备
$shippingService = app(ShippingService::class);

// 执行
$cost = $shippingService->calculateCost($weight, $destination);

// 断言
expect($cost)->toBe($expectedCost);
})->with('shippingRates');

// 定义外部数据提供者
function shippingRates() {
return [
[1, 'local', 5.99],
[1, 'national', 12.99],
[1, 'international', 29.99],
[5, 'local', 14.99],
[5, 'national', 29.99],
[5, 'international', 79.99],
];
}

// 使用数据集组合
test('user registration with different roles', function ($userData, $role, $shouldPass) {
// 测试代码
})->with([
'admin user' => [['name' => 'Admin', 'email' => 'admin@example.com'], 'admin', true],
'regular user' => [['name' => 'User', 'email' => 'user@example.com'], 'user', true],
'guest user' => [['name' => 'Guest', 'email' => 'guest@example.com'], 'guest', true],
]);

测试过滤器

Pest v3 提供了强大的测试过滤器功能,允许你控制哪些测试运行,哪些测试跳过,以及如何组织测试。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// 标记测试 - 使用分组管理测试
test('critical payment processing', function () {
// 测试代码
$paymentService = app(PaymentService::class);
$result = $paymentService->process([
'amount' => 100,
'currency' => 'USD',
'card' => '4242424242424242',
]);
expect($result)->toBeSuccessful();
})->group('critical')->group('payment');

test('api endpoint', function () {
// 测试代码
$response = $this->get('/api/v1/users');
$response->assertStatus(200);
})->group('api')->group('user');

test('slow integration test', function () {
// 测试代码
$integrationService = app(IntegrationService::class);
$result = $integrationService->syncData();
expect($result)->toBeSuccessful();
})->group('slow')->group('integration');

// 运行特定组的测试
// 运行单个组
// ./vendor/bin/pest --group=critical

// 运行多个组
// ./vendor/bin/pest --group=critical,api

// 排除特定组
// ./vendor/bin/pest --exclude-group=slow

// 组合包含和排除
// ./vendor/bin/pest --group=api --exclude-group=slow

// 跳过测试 - 临时跳过未实现的测试
test('not yet implemented', function () {
// 测试代码
$this->markTestIncomplete('Not implemented yet');
})->skip('Not implemented yet');

// 条件测试 - 根据环境或条件跳过测试
test('runs only on php 8.2+', function () {
// 测试代码
$featureService = app(FeatureService::class);
$result = $featureService->usePhp82Features();
expect($result)->toBeSuccessful();
})->skip(PHP_VERSION_ID < 80200, 'Requires PHP 8.2+');

test('runs only in staging environment', function () {
// 测试代码
$stagingService = app(StagingService::class);
$result = $stagingService->runStagingTasks();
expect($result)->toBeSuccessful();
})->skip(env('APP_ENV') !== 'staging', 'Only runs in staging environment');

// 聚焦测试 - 只运行特定的测试
test('focused test', function () {
// 测试代码
$focusedService = app(FocusedService::class);
$result = $focusedService->doSomething();
expect($result)->toBeSuccessful();
})->only();

// 多个聚焦测试
test('first focused test', function () {
// 测试代码
})->only();

test('second focused test', function () {
// 测试代码
})->only();

// 使用 todo 标记待实现的测试
test('todo: implement file upload functionality', function () {
// 测试代码将在未来实现
})->todo();

// 使用 depends 标记测试依赖关系
test('setup database', function () {
// 测试代码
$this->artisan('migrate:fresh');
$this->assertDatabaseHas('users', ['email' => 'admin@example.com']);
});

test('test user creation', function () {
// 测试代码
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);
expect($user)->toBeInstanceOf(User::class);
})->depends('setup database');

// 使用 timeout 设置测试超时时间
test('long running process', function () {
// 测试代码
$longRunningService = app(LongRunningService::class);
$result = $longRunningService->processLargeData();
expect($result)->toBeSuccessful();
})->timeout(60); // 60秒超时

// 组合多个过滤器
test('complex test with multiple filters', function () {
// 测试代码
$complexService = app(ComplexService::class);
$result = $complexService->doComplexTask();
expect($result)->toBeSuccessful();
})->group('critical')->group('complex')->timeout(30)->skip(env('CI') === 'true', 'Not run in CI');

并行测试

Pest v3 提供了强大的并行测试功能,允许你同时运行多个测试进程,显著提高测试执行速度,特别是在大型项目中。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 配置并行测试
// pest.php
return [
'parallel' => [
'processes' => 4, // 并行进程数,建议设置为 CPU 核心数
'order' => 'random', // 测试顺序:random, sequential
'timeout' => 60, // 单个测试超时时间(秒)
'output' => 'buffer', // 输出模式:buffer, concurrent
'stop-on-failure' => false, // 遇到失败时是否停止
],
];

// 运行并行测试
// 基本用法
// ./vendor/bin/pest --parallel

// 运行特定组的并行测试
// ./vendor/bin/pest --parallel --group=api

// 排除特定组的并行测试
// ./vendor/bin/pest --parallel --exclude-group=slow

// 限制并行进程数
// ./vendor/bin/pest --parallel --processes=2

// 监控并行测试进度
// ./vendor/bin/pest --parallel --verbose

// 并行测试的最佳实践

// 1. 确保测试隔离
// 使用 RefreshDatabase 特性确保每个测试都有干净的数据库
uses(Tests\TestCase::class, RefreshDatabase::class)->in('Feature');

// 2. 避免共享状态
// 不要在测试之间共享全局变量或静态状态
function testSharedState() {
// 错误示例:使用静态变量
static $counter = 0;
$counter++;
expect($counter)->toBe(1); // 第一次通过,第二次失败
}

// 3. 处理文件系统操作
// 在并行测试中,文件系统操作可能会冲突
// 解决方案:为每个测试使用唯一的临时目录
test('file upload', function () {
$tempDir = sys_get_temp_dir() . '/' . uniqid('test-', true);
mkdir($tempDir);

// 测试文件上传
$file = UploadedFile::fake()->image('test.jpg');
$response = $this->post('/upload', ['file' => $file]);

// 清理
if (is_dir($tempDir)) {
rmdir($tempDir);
}
});

// 4. 处理外部服务
// 在并行测试中,外部服务调用可能会冲突
// 解决方案:使用 Mock 或 Stub 模拟外部服务
test('external api integration', function () {
// 模拟外部 API 调用
Http::fake([
'api.example.com/*' => Http::response(['data' => 'test'], 200),
]);

$service = app(ExternalApiService::class);
$result = $service->callApi();

expect($result)->toBe('test');
});

// 5. 监控并行测试性能
// 使用 --profile 选项查看测试执行时间
// ./vendor/bin/pest --parallel --profile

// 6. 处理测试依赖
// 避免在并行测试中使用 depends(),因为测试顺序不确定
// 替代方案:使用 beforeEach() 或 beforeAll() 设置测试环境

describe('User Management', function () {
beforeEach(function () {
// 为每个测试设置必要的环境
$this->user = User::factory()->create();
});

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

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

测试生命周期钩子

Pest v3 提供了强大的测试生命周期钩子,允许你在测试执行的不同阶段运行代码,如测试前准备、测试后清理等。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// 测试套件级别的生命周期钩子

// 在所有测试开始前执行一次
beforeAll(function () {
// 全局设置
// 例如:初始化外部服务连接、加载测试数据等

// 初始化测试数据库
Artisan::call('migrate:fresh');

// 加载基础测试数据
Artisan::call('db:seed', ['--class' => 'TestSeeder']);

// 初始化外部服务模拟
Http::fake([
'api.example.com/*' => Http::response(['status' => 'ok'], 200),
]);
});

// 在所有测试结束后执行一次
afterAll(function () {
// 全局清理
// 例如:关闭外部服务连接、清理临时文件等

// 清理测试数据库
Artisan::call('migrate:rollback');

// 清理临时文件
$tempDir = storage_path('app/test-temp');
if (is_dir($tempDir)) {
File::cleanDirectory($tempDir);
}
});

// 在每个测试开始前执行
beforeEach(function () {
// 每个测试的准备工作
// 例如:创建测试用户、设置测试数据等

// 创建测试用户
$this->user = User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

// 创建测试产品
$this->product = Product::factory()->create([
'name' => 'Test Product',
'price' => 100,
'stock' => 10,
]);

// 设置测试环境
$this->actingAs($this->user);
});

// 在每个测试结束后执行
afterEach(function () {
// 每个测试的清理工作
// 例如:清理测试数据、重置状态等

// 清理测试文件
$this->cleanupTestFiles();

// 重置外部服务模拟
Http::fake();
});

// 分组级别的生命周期钩子
describe('User Management', function () {
// 仅在当前分组的测试前执行
beforeEach(function () {
// 分组特定的准备工作
$this->userService = app(UserService::class);
});

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

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

// 嵌套分组的生命周期钩子
describe('Authentication', function () {
// 外层分组的 beforeEach
beforeEach(function () {
$this->authService = app(AuthService::class);
});

describe('Login', function () {
// 内层分组的 beforeEach,会在每个 Login 测试前执行
beforeEach(function () {
$this->loginRoute = '/login';
});

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

describe('Registration', function () {
// 内层分组的 beforeEach,会在每个 Registration 测试前执行
beforeEach(function () {
$this->registerRoute = '/register';
});

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

// 测试生命周期钩子的最佳实践

// 1. 职责分离
// 每个钩子函数只负责一件事,保持代码清晰
beforeEach(function () {
$this->setupTestUser();
$this->setupTestData();
$this->setupTestEnvironment();
});

function setupTestUser() {
$this->user = User::factory()->create();
}

function setupTestData() {
$this->product = Product::factory()->create();
}

function setupTestEnvironment() {
$this->actingAs($this->user);
}

// 2. 避免重复代码
// 将重复的准备代码提取到辅助函数中
describe('Product Management', function () {
beforeEach(function () {
$this->product = $this->createTestProduct();
});

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

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

function createTestProduct(array $attributes = []) {
return Product::factory()->create($attributes);
}

// 3. 清理资源
// 确保每个测试后清理资源,避免测试之间的干扰
afterEach(function () {
$this->cleanupTestFiles();
$this->resetTestDatabase();
});

function cleanupTestFiles() {
$tempDir = storage_path('app/test-temp');
if (is_dir($tempDir)) {
File::cleanDirectory($tempDir);
}
}

function resetTestDatabase() {
// 重置数据库状态
}

// 4. 使用条件钩子
// 根据测试类型或环境执行不同的准备工作
beforeEach(function () {
if (in_array($this->test->getGroup(), ['api', 'integration'])) {
$this->setupApiTestEnvironment();
} else {
$this->setupUnitTestEnvironment();
}
});

function setupApiTestEnvironment() {
// API 测试的准备工作
}

function setupUnitTestEnvironment() {
// 单元测试的准备工作
}

3. 单元测试

单元测试是测试金字塔的基础,专注于测试单个类或方法的功能,确保每个组件都能按预期工作。

3.1 编写高质量单元测试

编写高质量的单元测试需要遵循一定的原则和最佳实践,以确保测试的有效性、可靠性和可维护性。以下是编写高质量单元测试的核心原则:

  1. 隔离性:单元测试应该独立运行,不依赖外部资源(如数据库、网络服务),使用 Mock/Stub 隔离外部依赖
  2. 原子性:每个单元测试应该只测试一个特定的功能点,确保测试的专注性和准确性
  3. 可重复性:单元测试应该能够重复运行并产生相同的结果,不受外部环境影响
  4. 清晰性:测试代码应该清晰易懂,包含明确的准备、执行和断言步骤
  5. 全面性:测试应该覆盖正常场景、边界情况和异常情况,确保代码的健壮性
  6. 可维护性:测试代码应该易于维护,遵循与生产代码相同的编码标准

测试服务类

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// tests/Unit/Services/UserServiceTest.php
use App\Services\UserService;
use App\Models\User;
use App\Services\MailService;
use Illuminate\Support\Facades\Hash;

require_once __DIR__ . '/../../TestCase.php';

describe('UserService', function () {
// 模拟依赖
$mailServiceMock = null;
$userService = null;

beforeEach(function () use (&$mailServiceMock, &$userService) {
// 创建 MailService 模拟
$mailServiceMock = Mockery::mock(MailService::class);

// 注入模拟依赖
$userService = new UserService($mailServiceMock);
});

afterEach(function () use (&$mailServiceMock) {
// 清理模拟
Mockery::close();
});

test('can create user with valid data', function () use (&$mailServiceMock, &$userService) {
// 准备
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

// 期望 MailService->sendWelcomeEmail 被调用一次
$mailServiceMock->shouldReceive('sendWelcomeEmail')
->once()
->with(Mockery::type(User::class));

// 模拟密码哈希
Hash::shouldReceive('make')
->once()
->with('password123')
->andReturn('hashed-password');

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

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

test('throws validation exception for invalid user data', function () use (&$userService) {
// 准备 - 无效数据
$invalidUserData = [
'name' => '', // 空名称
'email' => 'invalid-email', // 无效邮箱
'password' => 'short', // 密码太短
];

// 执行 & 断言 - 期望抛出验证异常
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Validation failed');

$userService->create($invalidUserData);
});

test('can find user by email', function () use (&$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);
expect($foundUser->email)->toBe('test@example.com');
});

test('returns null when user not found by email', function () use (&$userService) {
// 执行 - 查找不存在的用户
$foundUser = $userService->findByEmail('non-existent@example.com');

// 断言
expect($foundUser)->toBeNull();
});

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

// 执行
$updatedUser = $userService->updateProfile($user->id, $updateData);

// 断言
expect($updatedUser)->toBeInstanceOf(User::class);
expect($updatedUser->id)->toBe($user->id);
expect($updatedUser->name)->toBe('Updated Name');
expect($updatedUser->email)->toBe('updated@example.com');
});
});

测试工具类

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// tests/Unit/Helpers/StrHelperTest.php
use App\Helpers\StrHelper;

describe('StrHelper', function () {
test('can generate slug from string', function () {
// 测试用例
$testCases = [
['input' => 'Hello World', 'expected' => 'hello-world'],
['input' => 'Hello World', 'expected' => 'hello-world'],
['input' => 'Hello-World', 'expected' => 'hello-world'],
['input' => 'Hello@World', 'expected' => 'hello-world'],
['input' => 'Hello 123 World', 'expected' => 'hello-123-world'],
['input' => ' Hello World ', 'expected' => 'hello-world'],
['input' => 'HELLO WORLD', 'expected' => 'hello-world'],
['input' => 'hello world', 'expected' => 'hello-world'],
];

foreach ($testCases as $testCase) {
// 执行
$slug = StrHelper::slug($testCase['input']);

// 断言
expect($slug)->toBe($testCase['expected']);
}
});

test('can generate random string with specified length', function () {
// 测试不同长度
$lengths = [5, 10, 20, 32, 64];

foreach ($lengths as $length) {
// 执行
$random = StrHelper::random($length);

// 断言
expect($random)->toHaveLength($length);
expect(ctype_alnum($random))->toBeTrue(); // 确保只包含字母和数字
}
});

test('can truncate string with ellipsis', function () {
// 测试用例
$testCases = [
['input' => 'Hello World', 'length' => 5, 'expected' => 'Hello...'],
['input' => 'Hello World', 'length' => 10, 'expected' => 'Hello W...'],
['input' => 'Hello World', 'length' => 11, 'expected' => 'Hello World'],
['input' => 'Hello World', 'length' => 20, 'expected' => 'Hello World'],
['input' => '', 'length' => 5, 'expected' => ''],
];

foreach ($testCases as $testCase) {
// 执行
$truncated = StrHelper::truncate($testCase['input'], $testCase['length']);

// 断言
expect($truncated)->toBe($testCase['expected']);
}
});

test('can convert camelCase to snake_case', function () {
// 测试用例
$testCases = [
['input' => 'camelCase', 'expected' => 'camel_case'],
['input' => 'CamelCase', 'expected' => 'camel_case'],
['input' => 'camelCASE', 'expected' => 'camel_case'],
['input' => 'simple', 'expected' => 'simple'],
['input' => 'veryLongCamelCaseString', 'expected' => 'very_long_camel_case_string'],
];

foreach ($testCases as $testCase) {
// 执行
$snakeCase = StrHelper::camelToSnake($testCase['input']);

// 断言
expect($snakeCase)->toBe($testCase['expected']);
}
});
});

3.2 高级模拟技术

高级模拟技术是编写高质量单元测试的关键,它允许你隔离外部依赖,控制测试环境,确保测试的可重复性和可靠性。Laravel 提供了多种模拟工具,包括 Mockery、PHPUnit 模拟对象和 Laravel 内置的 Facade 模拟。

使用 Mockery 进行高级模拟

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// tests/Unit/Services/PaymentServiceTest.php
use App\Services\PaymentService;
use App\Services\PaymentGateway;
use App\Models\Order;
use App\Exceptions\PaymentFailedException;

describe('PaymentService', function () {
// 共享的测试前准备
$paymentGatewayMock = null;
$paymentService = null;

beforeEach(function () use (&$paymentGatewayMock, &$paymentService) {
// 创建 PaymentGateway 模拟
$paymentGatewayMock = Mockery::mock(PaymentGateway::class);

// 注入模拟依赖
$paymentService = new PaymentService($paymentGatewayMock);
});

afterEach(function () {
// 清理模拟
Mockery::close();
});

test('can process successful payment', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$order = Order::factory()->create(['total' => 100, 'currency' => 'USD']);
$paymentDetails = [
'card_number' => '4111111111111111',
'expiry_date' => '12/25',
'cvv' => '123',
'billing_address' => '123 Test St',
];

// 期望 PaymentGateway->charge 被调用并返回成功
$paymentGatewayMock->shouldReceive('charge')
->once()
->with(100, 'USD', $paymentDetails)
->andReturn([
'success' => true,
'transaction_id' => 'tx_123456',
'amount' => 100,
'currency' => 'USD',
'timestamp' => now()->toIso8601String(),
]);

// 执行
$result = $paymentService->processPayment($order, $paymentDetails);

// 断言
expect($result)->toBeTrue();
expect($order->fresh()->status)->toBe('paid');
expect($order->fresh()->transaction_id)->toBe('tx_123456');
expect($order->fresh()->paid_at)->toBeInstanceOf(Carbon::class);
});

test('handles payment failure', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$order = Order::factory()->create(['total' => 100, 'currency' => 'USD']);
$paymentDetails = [
'card_number' => '4111111111111111',
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->charge 被调用并返回失败
$paymentGatewayMock->shouldReceive('charge')
->once()
->with(100, 'USD', $paymentDetails)
->andReturn([
'success' => false,
'error' => 'Insufficient funds',
'error_code' => 'INSUFFICIENT_FUNDS',
]);

// 执行
$result = $paymentService->processPayment($order, $paymentDetails);

// 断言
expect($result)->toBeFalse();
expect($order->fresh()->status)->toBe('payment_failed');
expect($order->fresh()->failure_reason)->toBe('Insufficient funds');
});

test('throws exception for invalid payment details', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$order = Order::factory()->create(['total' => 100, 'currency' => 'USD']);
$invalidPaymentDetails = [
'card_number' => '1234', // 无效卡号
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->validate 被调用并抛出异常
$paymentGatewayMock->shouldReceive('validate')
->once()
->with($invalidPaymentDetails)
->andThrow(new PaymentFailedException('Invalid card number'));

// 执行 & 断言
$this->expectException(PaymentFailedException::class);
$this->expectExceptionMessage('Invalid card number');

$paymentService->processPayment($order, $invalidPaymentDetails);
});

test('retries payment on temporary failure', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$order = Order::factory()->create(['total' => 100, 'currency' => 'USD']);
$paymentDetails = [
'card_number' => '4111111111111111',
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->charge 被调用两次:第一次失败,第二次成功
$paymentGatewayMock->shouldReceive('charge')
->twice()
->with(100, 'USD', $paymentDetails)
->andReturnValues([
// 第一次调用返回临时失败
[
'success' => false,
'error' => 'Gateway timeout',
'error_code' => 'TEMPORARY_FAILURE',
],
// 第二次调用返回成功
[
'success' => true,
'transaction_id' => 'tx_123456',
'amount' => 100,
'currency' => 'USD',
],
]);

// 执行
$result = $paymentService->processPayment($order, $paymentDetails);

// 断言
expect($result)->toBeTrue();
expect($order->fresh()->status)->toBe('paid');
expect($order->fresh()->transaction_id)->toBe('tx_123456');
});

test('can refund payment', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$order = Order::factory()->create([
'total' => 100,
'currency' => 'USD',
'status' => 'paid',
'transaction_id' => 'tx_123456',
]);

// 期望 PaymentGateway->refund 被调用并返回成功
$paymentGatewayMock->shouldReceive('refund')
->once()
->with('tx_123456', 100, 'USD')
->andReturn([
'success' => true,
'refund_id' => 'rf_789012',
'amount' => 100,
'currency' => 'USD',
]);

// 执行
$result = $paymentService->refundPayment($order, 100);

// 断言
expect($result)->toBeTrue();
expect($order->fresh()->status)->toBe('refunded');
expect($order->fresh()->refund_id)->toBe('rf_789012');
expect($order->fresh()->refunded_at)->toBeInstanceOf(Carbon::class);
});
});

使用 Laravel 的模拟辅助函数

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
// tests/Unit/Controllers/UserControllerTest.php
use App\Http\Controllers\UserController;
use App\Services\UserService;
use App\Models\User;
use Illuminate\Http\Request;

describe('UserController', function () {
test('can get user profile', function () {
// 准备
$user = User::factory()->create();
$request = Request::create('/profile', 'GET');

// 模拟 UserService
$this->mock(UserService::class, function ($mock) use ($user) {
$mock->shouldReceive('getUserProfile')
->once()
->with($user->id)
->andReturn($user);
});

// 创建控制器实例
$controller = new UserController();

// 执行
$response = $controller->profile($request, $user->id);

// 断言
expect($response->getStatusCode())->toBe(200);
expect($response->getData()->user->id)->toBe($user->id);
});
});

3.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// tests/Unit/Services/MathServiceTest.php
use App\Services\MathService;
use App\Exceptions\InvalidInputException;

describe('MathService', function () {
$mathService = null;

beforeEach(function () use (&$mathService) {
$mathService = new MathService();
});

test('can divide positive integers', function () use (&$mathService) {
expect($mathService->divide(10, 2))->toBe(5);
expect($mathService->divide(100, 10))->toBe(10);
expect($mathService->divide(999, 3))->toBe(333);
});

test('can divide negative numbers', function () use (&$mathService) {
expect($mathService->divide(-10, 2))->toBe(-5);
expect($mathService->divide(10, -2))->toBe(-5);
expect($mathService->divide(-10, -2))->toBe(5);
expect($mathService->divide(-100, 25))->toBe(-4);
});

test('throws exception when dividing by zero', function () use (&$mathService) {
$this->expectException(DivisionByZeroError::class);
$this->expectExceptionMessage('Division by zero');
$mathService->divide(10, 0);
});

test('handles zero as dividend', function () use (&$mathService) {
expect($mathService->divide(0, 5))->toBe(0);
expect($mathService->divide(0, -10))->toBe(0);
expect($mathService->divide(0, 999))->toBe(0);
});

test('handles decimal numbers with precision', function () use (&$mathService) {
expect($mathService->divide(7, 2))->toBe(3.5);
expect($mathService->divide(10, 3))->toBeApproximately(3.3333333333);
expect($mathService->divide(1, 8))->toBe(0.125);
});

test('handles large numbers', function () use (&$mathService) {
expect($mathService->divide(1000000, 1000))->toBe(1000);
expect($mathService->divide(9999999999, 9999999999))->toBe(1);
});

test('throws exception for non-numeric inputs', function () use (&$mathService) {
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage('Both inputs must be numeric');
$mathService->divide('10', 2);
});

test('can calculate square root of positive numbers', function () use (&$mathService) {
expect($mathService->sqrt(4))->toBe(2);
expect($mathService->sqrt(9))->toBe(3);
expect($mathService->sqrt(16))->toBe(4);
expect($mathService->sqrt(2))->toBeApproximately(1.41421356237);
});

test('throws exception for negative square root', function () use (&$mathService) {
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage('Cannot calculate square root of negative number');
$mathService->sqrt(-4);
});

test('handles zero square root', function () use (&$mathService) {
expect($mathService->sqrt(0))->toBe(0);
});

test('can calculate factorial of non-negative integers', function () use (&$mathService) {
expect($mathService->factorial(0))->toBe(1); // 0! = 1
expect($mathService->factorial(1))->toBe(1);
expect($mathService->factorial(5))->toBe(120);
expect($mathService->factorial(10))->toBe(3628800);
});

test('throws exception for negative factorial', function () use (&$mathService) {
$this->expectException(InvalidInputException::class);
$this->expectExceptionMessage('Factorial is only defined for non-negative integers');
$mathService->factorial(-1);
});
});

// 测试字符串处理的边界情况
// tests/Unit/Helpers/StringHelperTest.php
use App\Helpers\StringHelper;

describe('StringHelper', function () {
test('can truncate string to exact length', function () {
expect(StringHelper::truncate('Hello World', 5))->toBe('Hello...');
expect(StringHelper::truncate('Hello World', 10))->toBe('Hello W...');
expect(StringHelper::truncate('Hello World', 11))->toBe('Hello World');
expect(StringHelper::truncate('Hello World', 20))->toBe('Hello World');
});

test('handles empty string truncation', function () {
expect(StringHelper::truncate('', 5))->toBe('');
});

test('handles very long string truncation', function () {
$longString = str_repeat('a', 1000);
$truncated = StringHelper::truncate($longString, 100);
expect($truncated)->toHaveLength(103); // 100 chars + '...'
expect(substr($truncated, -3))->toBe('...');
});

test('can generate slug from various string formats', function () {
expect(StringHelper::slug('Hello World'))->toBe('hello-world');
expect(StringHelper::slug('Hello World'))->toBe('hello-world');
expect(StringHelper::slug('Hello-World'))->toBe('hello-world');
expect(StringHelper::slug('Hello@World'))->toBe('hello-world');
expect(StringHelper::slug('Hello 123 World'))->toBe('hello-123-world');
expect(StringHelper::slug(' Hello World '))->toBe('hello-world');
expect(StringHelper::slug('HELLO WORLD'))->toBe('hello-world');
expect(StringHelper::slug('hello world'))->toBe('hello-world');
});

test('handles empty string slug generation', function () {
expect(StringHelper::slug(''))->toBe('');
});

test('handles string with only special characters', function () {
expect(StringHelper::slug('@#$%^&*()'))->toBe('');
});
});

测试异常处理

异常处理是编写健壮代码的重要组成部分,测试异常处理确保你的代码能够正确捕获和处理各种异常情况,提高应用的可靠性和用户体验。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// tests/Unit/Repositories/UserRepositoryTest.php
use App\Repositories\UserRepository;
use App\Models\User;
use App\Exceptions\UserCreationException;
use App\Exceptions\UserUpdateException;
use App\Exceptions\UserDeleteException;

describe('UserRepository', function () {
$userRepository = null;

beforeEach(function () use (&$userRepository) {
$userRepository = new UserRepository();
});

test('throws exception when creating user with duplicate email', function () use (&$userRepository) {
// 准备 - 创建一个用户
User::factory()->create(['email' => 'existing@example.com']);

// 尝试用相同邮箱创建另一个用户
$userData = [
'name' => 'Test User',
'email' => 'existing@example.com', // 重复邮箱
'password' => 'password123',
];

// 执行 & 断言
$this->expectException(UserCreationException::class);
$this->expectExceptionMessage('User with this email already exists');
$userRepository->create($userData);
});

test('throws exception when creating user with invalid data', function () use (&$userRepository) {
// 准备 - 无效用户数据
$invalidUserData = [
'name' => '', // 空名称
'email' => 'invalid-email', // 无效邮箱
'password' => 'short', // 密码太短
];

// 执行 & 断言
$this->expectException(UserCreationException::class);
$this->expectExceptionMessage('Invalid user data');
$userRepository->create($invalidUserData);
});

test('throws exception when updating non-existent user', function () use (&$userRepository) {
// 尝试更新不存在的用户
$updateData = ['name' => 'Updated Name'];

// 执行 & 断言
$this->expectException(UserUpdateException::class);
$this->expectExceptionMessage('User not found');
$userRepository->update(999999, $updateData); // 不存在的 ID
});

test('throws exception when updating user with duplicate email', function () use (&$userRepository) {
// 准备 - 创建两个用户
$user1 = User::factory()->create(['email' => 'user1@example.com']);
User::factory()->create(['email' => 'user2@example.com']);

// 尝试将 user1 的邮箱更新为 user2 的邮箱
$updateData = ['email' => 'user2@example.com'];

// 执行 & 断言
$this->expectException(UserUpdateException::class);
$this->expectExceptionMessage('Email already in use');
$userRepository->update($user1->id, $updateData);
});

test('throws exception when deleting non-existent user', function () use (&$userRepository) {
// 执行 & 断言
$this->expectException(UserDeleteException::class);
$this->expectExceptionMessage('User not found');
$userRepository->delete(999999); // 不存在的 ID
});

test('throws exception when deleting user with active subscriptions', function () use (&$userRepository) {
// 准备 - 创建一个有活跃订阅的用户
$user = User::factory()->create();

// 模拟用户有活跃订阅
$this->mock(SubscriptionRepository::class, function ($mock) use ($user) {
$mock->shouldReceive('hasActiveSubscriptions')
->once()
->with($user->id)
->andReturn(true);
});

// 执行 & 断言
$this->expectException(UserDeleteException::class);
$this->expectExceptionMessage('Cannot delete user with active subscriptions');
$userRepository->delete($user->id);
});
});

// 测试服务层异常处理
// tests/Unit/Services/PaymentServiceTest.php
use App\Services\PaymentService;
use App\Services\PaymentGateway;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\InvalidPaymentDetailsException;

describe('PaymentService with Exception Handling', function () {
$paymentGatewayMock = null;
$paymentService = null;

beforeEach(function () use (&$paymentGatewayMock, &$paymentService) {
$paymentGatewayMock = Mockery::mock(PaymentGateway::class);
$paymentService = new PaymentService($paymentGatewayMock);
});

afterEach(function () {
Mockery::close();
});

test('throws exception for invalid payment details', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$invalidPaymentDetails = [
'card_number' => '1234', // 无效卡号
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->validate 被调用并抛出异常
$paymentGatewayMock->shouldReceive('validate')
->once()
->with($invalidPaymentDetails)
->andThrow(new InvalidPaymentDetailsException('Invalid card number'));

// 执行 & 断言
$this->expectException(InvalidPaymentDetailsException::class);
$this->expectExceptionMessage('Invalid card number');
$paymentService->processPayment(null, $invalidPaymentDetails);
});

test('throws exception for payment failure', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$validPaymentDetails = [
'card_number' => '4111111111111111',
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->charge 被调用并抛出异常
$paymentGatewayMock->shouldReceive('charge')
->once()
->with(Mockery::any(), Mockery::any(), $validPaymentDetails)
->andThrow(new PaymentFailedException('Insufficient funds'));

// 执行 & 断言
$this->expectException(PaymentFailedException::class);
$this->expectExceptionMessage('Insufficient funds');
$paymentService->processPayment(null, $validPaymentDetails);
});

test('throws exception for gateway timeout', function () use (&$paymentGatewayMock, &$paymentService) {
// 准备
$validPaymentDetails = [
'card_number' => '4111111111111111',
'expiry_date' => '12/25',
'cvv' => '123',
];

// 期望 PaymentGateway->charge 被调用并抛出异常
$paymentGatewayMock->shouldReceive('charge')
->once()
->with(Mockery::any(), Mockery::any(), $validPaymentDetails)
->andThrow(new \GuzzleHttp\Exception\ConnectException('Connection timed out', new \GuzzleHttp\Psr7\Request('POST', 'https://api.payment.com/charge')));

// 执行 & 断言
$this->expectException(PaymentFailedException::class);
$this->expectExceptionMessage('Payment gateway timeout');
$paymentService->processPayment(null, $validPaymentDetails);
});
});

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// tests/Feature/Controllers/UserControllerTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('UserController', function () {
use RefreshDatabase;

test('can get user profile page', function () {
// 准备
$user = User::factory()->create();

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

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

test('redirects guest to login when accessing profile', function () {
// 执行 - 未登录用户访问个人资料页
$response = $this->get('/profile');

// 断言
$response->assertRedirect('/login');
});

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

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

// 断言
$response->assertRedirect('/profile');
$response->assertSessionHas('success', 'Profile updated successfully');

// 验证数据库更新
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
]);

// 验证用户实例更新
$updatedUser = $user->fresh();
expect($updatedUser->name)->toBe('Updated Name');
expect($updatedUser->email)->toBe('updated@example.com');
});

test('cannot update profile with invalid data', function () {
// 准备
$user = User::factory()->create();
$invalidData = [
'name' => '', // 空名称
'email' => 'invalid-email', // 无效邮箱
];

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

// 断言
$response->assertStatus(302); // 重定向回表单
$response->assertSessionHasErrors(['name', 'email']);

// 验证数据库未更新
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
});
});

测试表单验证

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// tests/Feature/Controllers/AuthControllerTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('AuthController', function () {
use RefreshDatabase;

test('can register with valid data', function () {
// 准备
$registrationData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
];

// 执行
$response = $this->post('/register', $registrationData);

// 断言
$response->assertRedirect('/dashboard');
$response->assertSessionHas('success', 'Registration successful');

// 验证用户已创建
$this->assertDatabaseHas('users', [
'email' => 'test@example.com',
'name' => 'Test User',
]);

// 验证用户已登录
$this->assertAuthenticated();
});

test('cannot register with duplicate email', function () {
// 准备 - 创建一个用户
$existingUser = \App\Models\User::factory()->create(['email' => 'test@example.com']);

// 尝试用相同邮箱注册
$registrationData = [
'name' => 'Another User',
'email' => 'test@example.com', // 重复邮箱
'password' => 'password123',
'password_confirmation' => 'password123',
];

// 执行
$response = $this->post('/register', $registrationData);

// 断言
$response->assertStatus(302);
$response->assertSessionHasErrors(['email']);

// 验证未创建新用户
$this->assertDatabaseCount('users', 1);
});

test('cannot register with password mismatch', function () {
// 准备 - 密码不匹配
$registrationData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'different-password', // 密码不匹配
];

// 执行
$response = $this->post('/register', $registrationData);

// 断言
$response->assertStatus(302);
$response->assertSessionHasErrors(['password']);

// 验证未创建用户
$this->assertDatabaseCount('users', 0);
});
});

4.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// tests/Feature/Services/OrderServiceTest.php
use App\Models\User;
use App\Models\Product;
use App\Models\Order;
use App\Models\OrderItem;
use App\Services\OrderService;
use App\Exceptions\OutOfStockException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Database\Eloquent\Collection;

describe('OrderService', function () {
use RefreshDatabase;

$orderService = null;

beforeEach(function () use (&$orderService) {
$orderService = app(OrderService::class);
});

test('can create order with multiple products', function () use (&$orderService) {
// 准备
$user = User::factory()->create();
$product1 = Product::factory()->create(['price' => 100, 'stock' => 10]);
$product2 = Product::factory()->create(['price' => 200, 'stock' => 5]);

$orderData = [
'user_id' => $user->id,
'items' => [
[
'product_id' => $product1->id,
'quantity' => 2,
'price' => $product1->price,
],
[
'product_id' => $product2->id,
'quantity' => 1,
'price' => $product2->price,
],
],
'total' => 400,
'currency' => 'USD',
'status' => 'pending',
];

// 执行
$order = $orderService->createOrder($orderData);

// 断言
expect($order)->toBeInstanceOf(Order::class);
expect($order->user_id)->toBe($user->id);
expect($order->total)->toBe(400);
expect($order->currency)->toBe('USD');
expect($order->status)->toBe('pending');

// 验证订单项目已创建
$orderItems = $order->items;
expect($orderItems)->toBeInstanceOf(Collection::class);
expect($orderItems)->toHaveCount(2);

// 验证第一个订单项目
$orderItem1 = $orderItems->firstWhere('product_id', $product1->id);
expect($orderItem1)->toBeInstanceOf(OrderItem::class);
expect($orderItem1->quantity)->toBe(2);
expect($orderItem1->price)->toBe(100);
expect($orderItem1->subtotal)->toBe(200);

// 验证第二个订单项目
$orderItem2 = $orderItems->firstWhere('product_id', $product2->id);
expect($orderItem2)->toBeInstanceOf(OrderItem::class);
expect($orderItem2->quantity)->toBe(1);
expect($orderItem2->price)->toBe(200);
expect($orderItem2->subtotal)->toBe(200);

// 验证库存已减少
$updatedProduct1 = $product1->fresh();
$updatedProduct2 = $product2->fresh();
expect($updatedProduct1->stock)->toBe(8); // 10 - 2
expect($updatedProduct2->stock)->toBe(4); // 5 - 1
});

test('throws exception when product is out of stock', function () use (&$orderService) {
// 准备
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100, 'stock' => 1]); // 库存为 1

$orderData = [
'user_id' => $user->id,
'items' => [
[
'product_id' => $product->id,
'quantity' => 2, // 尝试购买 2 个,超出库存
'price' => $product->price,
],
],
'total' => 200,
'currency' => 'USD',
'status' => 'pending',
];

// 执行 & 断言
$this->expectException(OutOfStockException::class);
$this->expectExceptionMessage('Product is out of stock');
$orderService->createOrder($orderData);

// 验证没有创建订单
$this->assertDatabaseCount('orders', 0);
$this->assertDatabaseCount('order_items', 0);
});

test('can cancel order and restore stock', function () use (&$orderService) {
// 准备
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100, 'stock' => 10]);

// 创建订单
$orderData = [
'user_id' => $user->id,
'items' => [
[
'product_id' => $product->id,
'quantity' => 2,
'price' => $product->price,
],
],
'total' => 200,
'currency' => 'USD',
'status' => 'pending',
];

$order = $orderService->createOrder($orderData);

// 验证库存已减少
$updatedProduct = $product->fresh();
expect($updatedProduct->stock)->toBe(8); // 10 - 2

// 执行 - 取消订单
$result = $orderService->cancelOrder($order->id);

// 断言
expect($result)->toBeTrue();

// 验证订单状态已更新
$cancelledOrder = $order->fresh();
expect($cancelledOrder->status)->toBe('cancelled');

// 验证库存已恢复
$restoredProduct = $product->fresh();
expect($restoredProduct->stock)->toBe(10); // 8 + 2
});

test('can get user orders with pagination', function () use (&$orderService) {
// 准备
$user = User::factory()->create();

// 创建多个订单
for ($i = 0; $i < 15; $i++) {
$product = Product::factory()->create(['price' => 100, 'stock' => 10]);

$orderData = [
'user_id' => $user->id,
'items' => [
[
'product_id' => $product->id,
'quantity' => 1,
'price' => $product->price,
],
],
'total' => 100,
'currency' => 'USD',
'status' => 'pending',
];

$orderService->createOrder($orderData);
}

// 执行 - 获取分页订单
$orders = $orderService->getUserOrders($user->id, ['page' => 1, 'per_page' => 10]);

// 断言
expect($orders)->toBeInstanceOf(Illuminate\Pagination\LengthAwarePaginator::class);
expect($orders->total())->toBe(15);
expect($orders->perPage())->toBe(10);
expect($orders->currentPage())->toBe(1);
expect($orders->hasMorePages())->toBeTrue();
expect($orders->items())->toHaveCount(10);
});

test('can update order status', function () use (&$orderService) {
// 准备
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 100, 'stock' => 10]);

// 创建订单
$orderData = [
'user_id' => $user->id,
'items' => [
[
'product_id' => $product->id,
'quantity' => 1,
'price' => $product->price,
],
],
'total' => 100,
'currency' => 'USD',
'status' => 'pending',
];

$order = $orderService->createOrder($orderData);

// 执行 - 更新订单状态
$result = $orderService->updateOrderStatus($order->id, 'completed');

// 断言
expect($result)->toBeTrue();

// 验证订单状态已更新
$updatedOrder = $order->fresh();
expect($updatedOrder->status)->toBe('completed');
expect($updatedOrder->completed_at)->toBeInstanceOf(Carbon\Carbon::class);
});
});
        'user_id' => $user->id,
        'items' => [
            ['product_id' => $product1->id, 'quantity' => 2],
            ['product_id' => $product2->id, 'quantity' => 1],
        ],
    ];
    
    // 执行
    $order = $orderService->createOrder($orderData);
    
    // 断言
    expect($order)->toBeInstanceOf(Order::class);
    expect($order->user_id)->toBe($user->id);
    expect($order->total)->toBe(400); // 2*100 + 1*200
    
    // 验证订单项目
    $this->assertDatabaseHas('order_items', [
        'order_id' => $order->id,
        'product_id' => $product1->id,
        'quantity' => 2,
        'price' => 100,
    ]);
    
    $this->assertDatabaseHas('order_items', [
        'order_id' => $order->id,
        'product_id' => $product2->id,
        'quantity' => 1,
        'price' => 200,
    ]);
    
    // 验证库存减少
    $this->assertDatabaseHas('products', [
        'id' => $product1->id,
        'stock' => $product1->stock - 2,
    ]);
    
    $this->assertDatabaseHas('products', [
        'id' => $product2->id,
        'stock' => $product2->stock - 1,
    ]);
});

test('cannot create order with insufficient stock', function () use (&$orderService) {
    // 准备 - 创建库存为 1 的产品
    $user = User::factory()->create();
    $product = Product::factory()->create(['price' => 100, 'stock' => 1]);
    
    $orderData = [
        'user_id' => $user->id,
        'items' => [
            ['product_id' => $product->id, 'quantity' => 2], // 尝试购买 2 个,但库存只有 1 个
        ],
    ];
    
    // 执行 & 断言
    $this->expectException(\App\Exceptions\InsufficientStockException::class);
    $this->expectExceptionMessage('Insufficient stock for product: ' . $product->name);
    
    $orderService->createOrder($orderData);
    
    // 验证未创建订单
    $this->assertDatabaseCount('orders', 0);
});

});

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

### 4.3 测试中间件

#### 测试认证中间件

```php
// tests/Feature/Middleware/AuthMiddlewareTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('AuthMiddleware', function () {
use RefreshDatabase;

test('allows authenticated users to access protected routes', function () {
// 准备
$user = User::factory()->create();

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

// 断言
$response->assertStatus(200);
$response->assertViewIs('dashboard');
});

test('redirects guests to login for protected routes', function () {
// 执行 - 未登录用户访问受保护路由
$response = $this->get('/dashboard');

// 断言
$response->assertRedirect('/login');
$response->assertSessionHas('error', 'Please login to access this page');
});
});

测试权限中间件

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
// tests/Feature/Middleware/PermissionMiddlewareTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('PermissionMiddleware', function () {
use RefreshDatabase;

test('allows users with permission to access protected routes', function () {
// 准备 - 创建具有 admin 权限的用户
$adminUser = User::factory()->create(['role' => 'admin']);

// 执行
$response = $this->actingAs($adminUser)->get('/admin/dashboard');

// 断言
$response->assertStatus(200);
$response->assertViewIs('admin.dashboard');
});

test('denies users without permission from protected routes', function () {
// 准备 - 创建普通用户
$regularUser = User::factory()->create(['role' => 'user']);

// 执行 - 普通用户尝试访问管理员路由
$response = $this->actingAs($regularUser)->get('/admin/dashboard');

// 断言
$response->assertStatus(403);
$response->assertSee('You do not have permission to access this page');
});

test('redirects guests to login for admin routes', function () {
// 执行 - 未登录用户访问管理员路由
$response = $this->get('/admin/dashboard');

// 断言
$response->assertRedirect('/login');
});
});

4.4 测试事件与监听器

测试事件触发

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
57
58
59
60
61
62
63
64
// tests/Feature/Events/OrderPlacedEventTest.php
use App\Models\User;
use App\Models\Product;
use App\Models\Order;
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('OrderPlacedEvent', function () {
use RefreshDatabase;

test('OrderPlaced event is dispatched when order is created', function () {
// 准备
Event::fake();

$user = User::factory()->create();
$product = Product::factory()->create();

// 执行 - 创建订单
$order = Order::create([
'user_id' => $user->id,
'total' => $product->price,
'status' => 'pending',
]);

$order->items()->create([
'product_id' => $product->id,
'quantity' => 1,
'price' => $product->price,
]);

// 断言
Event::assertDispatched(OrderPlaced::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
});

test('OrderPlaced listeners are executed', function () {
// 准备
Event::fake();

$user = User::factory()->create();
$product = Product::factory()->create();

// 执行 - 创建订单
$order = Order::create([
'user_id' => $user->id,
'total' => $product->price,
'status' => 'pending',
]);

$order->items()->create([
'product_id' => $product->id,
'quantity' => 1,
'price' => $product->price,
]);

// 断言
Event::assertDispatched(OrderPlaced::class);

// 可以进一步测试监听器的具体行为
// 例如,验证发送了邮件、更新了库存等
});
});

5. API 测试

API 测试验证应用程序的接口是否按预期工作,确保与客户端的交互正常。

5.1 测试 RESTful API

基本 API 测试

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// tests/Feature/Api/UserApiTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('UserApi', function () {
use RefreshDatabase;

$token = null;
$user = null;

beforeEach(function () use (&$token, &$user) {
// 准备测试用户
$user = User::factory()->create();

// 生成 API 令牌
$token = $user->createToken('test-token')->plainTextToken;
});

test('can get user profile via API', function () use (&$token, &$user) {
// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->get('/api/profile');

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

test('cannot get user profile without authentication', function () {
// 执行 - 未提供认证令牌
$response = $this->withHeaders([
'Accept' => 'application/json',
])->get('/api/profile');

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

test('can update user profile via API', function () use (&$token, &$user) {
// 准备
$updateData = [
'name' => 'Updated Name',
'email' => 'updated@example.com',
];

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->put('/api/profile', $updateData);

// 断言
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'message' => 'Profile updated successfully',
'data' => [
'id' => $user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
],
]);

// 验证数据库更新
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
]);
});

test('cannot update user profile with invalid data', function () use (&$token) {
// 准备 - 无效数据
$invalidData = [
'name' => '', // 空名称
'email' => 'invalid-email', // 无效邮箱
];

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->put('/api/profile', $invalidData);

// 断言
$response->assertStatus(422);
$response->assertJsonValidationErrors(['name', 'email']);
$response->assertJsonStructure([
'success' => false,
'message' => 'Validation failed',
'errors' => [
'name',
'email',
],
]);
});
});

测试资源 API

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// tests/Feature/Api/ProductApiTest.php
use App\Models\User;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('ProductApi', function () {
use RefreshDatabase;

$token = null;

beforeEach(function () use (&$token) {
// 准备管理员用户
$admin = User::factory()->create(['role' => 'admin']);

// 生成 API 令牌
$token = $admin->createToken('test-token')->plainTextToken;
});

test('can get list of products', function () use (&$token) {
// 准备
Product::factory()->count(5)->create();

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->get('/api/products');

// 断言
$response->assertStatus(200);
$response->assertJson([
'success' => true,
]);
$response->assertJsonStructure([
'success',
'data' => [
'*' => [
'id',
'name',
'description',
'price',
'stock',
'created_at',
],
],
'meta' => [
'total',
'per_page',
'current_page',
'last_page',
'from',
'to',
],
]);

// 验证返回了 5 个产品
$responseData = $response->json();
expect(count($responseData['data']))->toBe(5);
});

test('can create product via API', function () use (&$token) {
// 准备
$productData = [
'name' => 'Test Product',
'description' => 'Test product description',
'price' => 100,
'stock' => 50,
];

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->post('/api/products', $productData);

// 断言
$response->assertStatus(201);
$response->assertJson([
'success' => true,
'message' => 'Product created successfully',
'data' => [
'name' => 'Test Product',
'price' => 100,
'stock' => 50,
],
]);

// 验证数据库创建
$this->assertDatabaseHas('products', [
'name' => 'Test Product',
'price' => 100,
'stock' => 50,
]);
});

test('can update product via API', function () use (&$token) {
// 准备
$product = Product::factory()->create();
$updateData = [
'name' => 'Updated Product',
'price' => 150,
'stock' => 75,
];

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->put('/api/products/' . $product->id, $updateData);

// 断言
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'message' => 'Product updated successfully',
'data' => [
'id' => $product->id,
'name' => 'Updated Product',
'price' => 150,
'stock' => 75,
],
]);

// 验证数据库更新
$this->assertDatabaseHas('products', [
'id' => $product->id,
'name' => 'Updated Product',
'price' => 150,
'stock' => 75,
]);
});

test('can delete product via API', function () use (&$token) {
// 准备
$product = Product::factory()->create();

// 执行
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->delete('/api/products/' . $product->id);

// 断言
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'message' => 'Product deleted successfully',
]);

// 验证数据库删除
$this->assertDatabaseMissing('products', [
'id' => $product->id,
]);
});
});

5.2 测试 API 认证

测试 API 登录

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// tests/Feature/Api/AuthApiTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('AuthApi', function () {
use RefreshDatabase;

test('can login via API with valid credentials', function () {
// 准备
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

$loginData = [
'email' => 'test@example.com',
'password' => 'password123',
];

// 执行
$response = $this->withHeaders([
'Accept' => 'application/json',
])->post('/api/login', $loginData);

// 断言
$response->assertStatus(200);
$response->assertJson([
'success' => true,
'message' => 'Login successful',
]);
$response->assertJsonStructure([
'success',
'message',
'data' => [
'access_token',
'token_type',
'expires_in',
'user' => [
'id',
'name',
'email',
'role',
],
],
]);

// 验证返回了访问令牌
$responseData = $response->json();
expect($responseData['data']['access_token'])->toBeString();
expect(strlen($responseData['data']['access_token']))->toBeGreaterThan(0);
});

test('cannot login via API with invalid credentials', function () {
// 准备
User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

$loginData = [
'email' => 'test@example.com',
'password' => 'wrong-password', // 错误密码
];

// 执行
$response = $this->withHeaders([
'Accept' => 'application/json',
])->post('/api/login', $loginData);

// 断言
$response->assertStatus(401);
$response->assertJson([
'success' => false,
'message' => 'Invalid credentials',
]);
});

test('cannot login via API with non-existent email', function () {
// 准备 - 未创建用户
$loginData = [
'email' => 'non-existent@example.com',
'password' => 'password123',
];

// 执行
$response = $this->withHeaders([
'Accept' => 'application/json',
])->post('/api/login', $loginData);

// 断言
$response->assertStatus(401);
$response->assertJson([
'success' => false,
'message' => 'Invalid credentials',
]);
});
});

5.3 测试 API 错误处理

测试错误响应

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
57
58
59
60
// tests/Feature/Api/ErrorHandlingTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

describe('ApiErrorHandling', function () {
use RefreshDatabase;

$token = null;

beforeEach(function () use (&$token) {
// 准备测试用户
$user = User::factory()->create();
$token = $user->createToken('test-token')->plainTextToken;
});

test('returns 404 for non-existent resource', function () use (&$token) {
// 执行 - 访问不存在的产品
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->get('/api/products/999999'); // 不存在的 ID

// 断言
$response->assertStatus(404);
$response->assertJson([
'success' => false,
'message' => 'Resource not found',
]);
});

test('returns 403 for forbidden access', function () use (&$token) {
// 执行 - 普通用户尝试访问管理员路由
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->get('/api/admin/dashboard');

// 断言
$response->assertStatus(403);
$response->assertJson([
'success' => false,
'message' => 'Access forbidden',
]);
});

test('returns 405 for method not allowed', function () use (&$token) {
// 执行 - 使用错误的 HTTP 方法
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->post('/api/profile'); // 应该使用 GET 或 PUT

// 断言
$response->assertStatus(405);
$response->assertJson([
'success' => false,
'message' => 'Method not allowed',
]);
});
});

6. 浏览器测试

浏览器测试使用 Laravel Dusk 模拟用户在浏览器中的操作,验证完整的用户流程。

6.1 配置 Dusk

安装与配置

1
2
3
4
5
6
7
8
# 安装 Dusk
composer require laravel/dusk --dev

# 安装 ChromeDriver
php artisan dusk:install

# 发布配置文件
php artisan vendor:publish --tag=dusk-config

配置测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .env.dusk.local
APP_ENV=local
APP_URL=http://localhost:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_test
DB_USERNAME=root
DB_PASSWORD=

CACHE_DRIVER=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// tests/Browser/AuthTest.php
namespace Tests\Browser;

use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class AuthTest extends DuskTestCase
{
/**
* 测试用户注册流程
*/
public function test_user_can_register()
{
$this->browse(function (Browser $browser) {
$browser->visit('/register')
->type('name', 'Test User')
->type('email', 'test' . time() . '@example.com')
->type('password', 'password123')
->type('password_confirmation', 'password123')
->press('Register')
->assertPathIs('/dashboard')
->assertSee('Welcome to your dashboard');
});
}

/**
* 测试用户登录流程
*/
public function test_user_can_login()
{
// 创建测试用户
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('email', 'test@example.com')
->type('password', 'password123')
->press('Login')
->assertPathIs('/dashboard')
->assertSee('Welcome to your dashboard');
});
}

/**
* 测试用户登出流程
*/
public function test_user_can_logout()
{
// 创建测试用户
$user = User::create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password123'),
]);

$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/dashboard')
->clickLink('Logout')
->assertPathIs('/login')
->assertSee('You have been logged out');
});
}
}

测试表单提交

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
57
58
59
60
61
62
63
64
65
66
67
68
69
// tests/Browser/ProductTest.php
namespace Tests\Browser;

use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class ProductTest extends DuskTestCase
{
/**
* 测试创建产品
*/
public function test_admin_can_create_product()
{
// 创建管理员用户
$admin = User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password123'),
'role' => 'admin',
]);

$this->browse(function (Browser $browser) use ($admin) {
$browser->loginAs($admin)
->visit('/admin/products/create')
->type('name', 'Test Product')
->type('description', 'Test product description')
->type('price', '100')
->type('stock', '50')
->press('Create Product')
->assertPathIs('/admin/products')
->assertSee('Product created successfully')
->assertSee('Test Product');
});
}

/**
* 测试编辑产品
*/
public function test_admin_can_edit_product()
{
// 创建管理员用户
$admin = User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password123'),
'role' => 'admin',
]);

// 创建测试产品
$product = \App\Models\Product::create([
'name' => 'Original Product',
'description' => 'Original description',
'price' => 50,
'stock' => 25,
]);

$this->browse(function (Browser $browser) use ($admin, $product) {
$browser->loginAs($admin)
->visit('/admin/products/' . $product->id . '/edit')
->type('name', 'Updated Product')
->type('price', '150')
->press('Update Product')
->assertPathIs('/admin/products')
->assertSee('Product updated successfully')
->assertSee('Updated Product');
});
}
}

7. 测试最佳实践

7.1 测试组织

测试目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tests/
├── Unit/ # 单元测试
│ ├── Services/ # 服务类测试
│ ├── Helpers/ # 工具类测试
│ ├── Models/ # 模型测试
│ └── Repositories/ # 仓库测试
├── Feature/ # 集成测试
│ ├── Controllers/ # 控制器测试
│ ├── Api/ # API 测试
│ ├── Services/ # 服务集成测试
│ ├── Middleware/ # 中间件测试
│ └── Events/ # 事件测试
├── Browser/ # 浏览器测试
├── Pest.php # Pest 配置
└── TestCase.php # 测试基类

7.2 测试命名约定

测试方法命名

  • 描述性命名: 测试方法应该清晰描述测试的内容
  • 使用动词: 描述测试的行为
  • 具体场景: 包含具体的测试场景

示例:

  • test_user_can_register_with_valid_data
  • test_user_cannot_register_with_duplicate_email
  • test_product_service_can_create_product
  • test_api_returns_404_for_non_existent_resource

7.3 测试编写技巧

测试准备与清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 好的测试结构
test('can create user', function () {
// 准备 (Arrange)
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
];

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

// 断言 (Assert)
expect($user)->toBeInstanceOf(User::class);
expect($user->name)->toBe('Test User');
});

使用测试辅助函数

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
// tests/Pest.php

// 辅助函数
function createTestUser(array $attributes = [])
{
return User::factory()->create($attributes);
}

function createTestProduct(array $attributes = [])
{
return Product::factory()->create($attributes);
}

function createTestOrder(array $attributes = [])
{
return Order::factory()->create($attributes);
}

function actingAsAdmin()
{
return test()->actingAs(
createTestUser(['role' => 'admin'])
);
}

function actingAsUser()
{
return test()->actingAs(
createTestUser(['role' => 'user'])
);
}

7.4 测试性能优化

并行测试

1
2
3
4
5
# 运行并行测试
./vendor/bin/pest --parallel

# 配置并行进程数
./vendor/bin/pest --parallel --processes=4

测试数据库优化

1
2
3
4
5
6
// 使用内存数据库进行测试
// phpunit.xml
<php>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>

测试缓存

1
2
3
4
5
// 缓存测试结果
// pest.php
return [
'cache' => true,
];

7.5 持续集成测试

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/tests.yml
name: 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, pdo_sqlite
coverage: xdebug

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

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

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

GitLab CI 配置

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

run_tests:
stage: test
image: php:8.2-cli
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
- ./vendor/bin/pest
artifacts:
reports:
junit: ./junit.xml
only:
- main
- develop
- merge_requests

8. 测试覆盖率

8.1 配置代码覆盖率

Xdebug 配置

1
2
3
4
; php.ini
[xdebug]
zend_extension=xdebug
xdebug.mode=coverage

运行覆盖率测试

1
2
3
4
5
6
7
8
# 运行测试并生成覆盖率报告
./vendor/bin/pest --coverage

# 生成 HTML 覆盖率报告
./vendor/bin/pest --coverage --coverage-html=coverage

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

8.2 覆盖率指标

关键覆盖率指标

指标描述目标值
行覆盖率测试执行的代码行数百分比≥ 80%
分支覆盖率测试执行的代码分支百分比≥ 70%
路径覆盖率测试执行的代码路径百分比≥ 60%
方法覆盖率测试覆盖的方法百分比≥ 85%

覆盖率分析

1
2
3
4
5
# 查看覆盖率摘要
./vendor/bin/pest --coverage --coverage-text

# 查看详细覆盖率
./vendor/bin/pest --coverage --coverage-details

8.3 提高覆盖率的策略

优先测试关键组件

  1. 核心业务逻辑: 优先测试业务核心功能
  2. API 接口: 确保所有 API 端点都有测试
  3. 认证与授权: 测试所有认证和授权场景
  4. 数据处理: 测试数据验证和处理逻辑
  5. 错误处理: 测试各种错误场景

测试边界情况

  • 输入边界: 测试最小值、最大值、空值等
  • 业务规则边界: 测试业务规则的边界条件
  • 异常情况: 测试各种异常场景
  • 性能边界: 测试系统在边界负载下的表现

9. 高级测试技术

9.1 测试驱动开发 (TDD)

TDD 工作流程

  1. 编写失败的测试: 首先编写一个失败的测试,描述期望的行为
  2. 编写最小化代码: 编写足够的代码使测试通过
  3. 重构代码: 优化代码结构,保持测试通过
  4. 重复循环: 对每个功能重复上述步骤

TDD 示例

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
// 1. 编写失败的测试
test('can calculate total price with discount', function () {
$order = new Order();
$order->addItem('Product 1', 100, 2); // 2 个,每个 100
$order->addItem('Product 2', 50, 1); // 1 个,每个 50

// 应用 10% 折扣
$total = $order->calculateTotal(10);

expect($total)->toBe(225); // 期望 (2*100 + 1*50) * 0.9 = 225
});

// 2. 编写代码使测试通过
class Order
{
private $items = [];

public function addItem($name, $price, $quantity)
{
$this->items[] = compact('name', 'price', 'quantity');
}

public function calculateTotal($discount = 0)
{
$subtotal = array_reduce($this->items, function ($total, $item) {
return $total + ($item['price'] * $item['quantity']);
}, 0);

return $subtotal - ($subtotal * $discount / 100);
}
}

// 3. 重构代码(如果需要)

9.2 行为驱动开发 (BDD)

BDD 工作流程

  1. 定义行为: 使用自然语言描述功能行为
  2. 编写场景: 创建具体的测试场景
  3. 实现功能: 实现功能使场景通过
  4. 验证行为: 确保所有场景都通过

BDD 示例

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
// 使用 Pest 的描述性语法
describe('User Registration', function () {
context('when user provides valid data', function () {
it('creates user successfully', function () {
// 测试代码
});

it('sends welcome email', function () {
// 测试代码
});

it('logs user in automatically', function () {
// 测试代码
});
});

context('when user provides invalid data', function () {
it('shows validation errors', function () {
// 测试代码
});

it('does not create user', function () {
// 测试代码
});
});
});

9.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
// 消费者测试
test('api returns user data in expected format', function () {
// 执行
$response = $this->get('/api/users/1');

// 验证响应结构
$response->assertJsonStructure([
'id',
'name',
'email',
'created_at',
'updated_at',
'roles' => [
'*' => [
'id',
'name',
'permissions' => [
'*' => [
'id',
'name',
],
],
],
],
]);
});

9.4 性能测试

基本性能测试

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/Feature/PerformanceTest.php
describe('Performance', function () {
test('api response time is under 500ms', function () {
$startTime = microtime(true);

// 执行 API 请求
$response = $this->get('/api/products');

$endTime = microtime(true);
$responseTime = ($endTime - $startTime) * 1000; // 转换为毫秒

// 断言响应时间
expect($responseTime)->toBeLessThan(500);
$response->assertStatus(200);
});

test('database query performance', function () {
// 准备大量数据
\App\Models\Product::factory()->count(1000)->create();

$startTime = microtime(true);

// 执行数据库查询
$products = \App\Models\Product::with('category')->get();

$endTime = microtime(true);
$queryTime = ($endTime - $startTime) * 1000; // 转换为毫秒

// 断言查询时间
expect($queryTime)->toBeLessThan(200);
expect($products)->toHaveCount(1000);
});
});

10. 测试工具与集成

10.1 常用测试工具

核心测试工具

工具用途版本
PestPHP 测试框架v3.0+
PHPUnitPHP 单元测试框架v10.0+
Laravel Dusk浏览器测试v7.0+
Mockery模拟库v1.6+
Faker测试数据生成v1.23+

辅助测试工具

工具用途版本
Xdebug代码覆盖率分析v3.2+
PHPStan静态代码分析v1.10+
Psalm静态代码分析v5.15+
Codeception全栈测试框架v5.0+
BehatBDD 测试框架v3.13+

10.2 IDE 集成

PHPStorm 测试集成

  1. 配置测试框架:

    • 打开 Settings > PHP > Test Frameworks
    • 配置 Pest 和 PHPUnit
  2. 运行测试:

    • 右键点击测试文件或方法
    • 选择 Run 'TestName'
  3. 测试覆盖率:

    • 运行测试时选择 Run with Coverage
    • 查看代码覆盖率高亮

VS Code 测试集成

  1. 安装扩展:

    • pestphp/pest-vscode - Pest 测试扩展
    • recca0120/vscode-phpunit - PHPUnit 测试扩展
  2. 配置:

    • settings.json 中配置测试路径
  3. 运行测试:

    • 使用命令面板: Pest: Run Test
    • 查看测试结果和覆盖率

10.3 测试报告集成

生成测试报告

1
2
3
4
5
6
7
8
# 生成 JUnit XML 报告
./vendor/bin/pest --log-junit=junit.xml

# 生成 HTML 报告
./vendor/bin/pest --coverage --coverage-html=coverage

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

CI/CD 集成

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
57
58
59
60
61
# GitHub Actions 完整配置
name: Laravel Tests

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

jobs:
test:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: laravel_test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

steps:
- uses: actions/checkout@v3

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

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

- name: Copy .env
run: |
cp .env.example .env
sed -i 's/DB_DATABASE=laravel/DB_DATABASE=laravel_test/g' .env
sed -i 's/DB_PASSWORD=/DB_PASSWORD=password/g' .env

- name: Generate key
run: php artisan key:generate

- name: Run migrations
run: php artisan migrate

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

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

- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: junit.xml

11. 总结

Laravel 12 提供了强大的测试工具和框架,使开发者能够编写高质量、可维护的测试。通过遵循本文档中的最佳实践和技术指南,您可以:

  1. 建立完整的测试体系: 从单元测试到端到端测试,覆盖所有测试层次
  2. 提高代码质量: 通过测试发现和修复潜在问题
  3. 加速开发流程: 减少手动测试时间,提高开发效率
  4. 增强系统稳定性: 确保代码变更不会破坏现有功能
  5. 提升团队协作: 测试作为文档,帮助团队理解代码行为

采用测试驱动开发和持续集成实践,将使您的 Laravel 应用更加健壮、可靠,并为用户提供更好的体验。
// 准备
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,
        ],
    ]);
});

});

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

#### 测试 POST 请求

```php
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 应用而努力。