Laravel 13 测试改进详解

摘要

Laravel 13 对测试系统进行了多项改进,包括更好的并行测试支持和测试隔离。本文将深入讲解 Laravel 13 的测试改进,包括:

  • 并行测试配置与优化
  • 测试隔离改进
  • Pest 与 PHPUnit 双支持
  • 测试辅助方法增强
  • 模拟与断言改进
  • 实战案例:构建完整测试套件

本文适合希望提升测试效率的 Laravel 开发者。

1. 测试改进概览

1.1 主要改进

改进描述
并行测试更好的隔离与性能
Pest 支持第一方 Pest 支持
测试辅助新增便捷方法
断言增强更多语义化断言

1.2 测试框架支持

Laravel 13 同时支持 PHPUnit 和 Pest:

1
2
3
4
5
# PHPUnit
php artisan test

# Pest
php artisan test --pest

2. 并行测试

2.1 启用并行测试

1
php artisan test --parallel

2.2 配置进程数

1
php artisan test --parallel --processes=4

2.3 配置文件

1
2
3
4
// phpunit.xml
<php>
<env name="TEST_PARALLEL_PROCESSES" value="4"/>
</php>

2.4 数据库隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// tests/TestCase.php
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\RefreshDatabase;

abstract class TestCase extends BaseTestCase
{
use RefreshDatabase;

protected function parallelProcessId(): int
{
return (int) env('TEST_PARALLEL_PROCESS_ID', 1);
}

protected function getDatabaseName(): string
{
return 'test_' . $this->parallelProcessId();
}
}

3. Pest 测试

3.1 安装 Pest

1
2
composer require pestphp/pest --dev
php artisan pest:install

3.2 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
// tests/Feature/UserTest.php
use App\Models\User;
use function Pest\Laravel\{actingAs, get, post};

it('can list users', function () {
$users = User::factory()->count(10)->create();

$response = get('/api/users');

$response->assertStatus(200)
->assertJsonCount(10, 'data');
});

it('requires authentication', function () {
get('/api/users')->assertUnauthorized();
});

it('can create a user', function () {
$data = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password',
];

post('/api/users', $data)
->assertCreated()
->assertJsonPath('data.email', 'john@example.com');
});

3.3 Pest 数据集

1
2
3
4
5
6
7
8
9
10
11
// tests/Datasets/UserData.php
dataset('user_emails', [
'john@example.com',
'jane@example.com',
'bob@example.com',
]);

it('validates email format', function ($email) {
post('/api/users', ['email' => $email])
->assertValid('email');
})->with('user_emails');

4. 测试辅助方法

4.1 新增断言

1
2
3
4
5
6
7
8
9
10
11
// 断言模型存在
$this->assertDatabaseHasModel(User::class, ['email' => 'john@example.com']);

// 断言模型不存在
$this->assertDatabaseMissingModel(User::class, ['email' => 'nonexistent@example.com']);

// 断言软删除
$this->assertSoftDeleted($user);

// 断言未软删除
$this->assertNotSoftDeleted($user);

4.2 便捷方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建并认证用户
$user = $this->actingAsUser();
$admin = $this->actingAsAdmin();

// 断言 JSON 结构
$response->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email'],
],
]);

// 断言 JSON 片段
$response->assertJsonFragment(['name' => 'John']);

4.3 模型断言

1
2
3
4
5
6
7
8
// 断言模型属性
$this->assertModelEquals($expected, $actual);

// 断言模型集合
$this->assertModelCollectionEquals(
User::whereIn('id', [1, 2, 3])->get(),
$actualCollection
);

5. 模拟增强

5.1 队列模拟

1
2
3
4
5
6
7
8
9
10
11
use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessPodcast;

Queue::fake();

// 执行代码...

Queue::assertPushed(ProcessPodcast::class);
Queue::assertPushedOn('podcasts', ProcessPodcast::class);
Queue::assertPushedTimes(ProcessPodcast::class, 2);
Queue::assertNotPushed(AnotherJob::class);

5.2 事件模拟

1
2
3
4
5
6
7
8
9
10
use Illuminate\Support\Facades\Event;
use App\Events\UserRegistered;

Event::fake();

// 执行代码...

Event::assertDispatched(UserRegistered::class);
Event::assertDispatchedTimes(UserRegistered::class, 1);
Event::assertNotDispatched(AnotherEvent::class);

5.3 HTTP 模拟

1
2
3
4
5
6
7
8
9
10
11
use Illuminate\Support\Facades\Http;

Http::fake([
'api.example.com/*' => Http::response(['status' => 'ok'], 200),
]);

// 执行代码...

Http::assertSent(function ($request) {
return $request->url() === 'https://api.example.com/users';
});

6. 测试隔离

6.1 数据库事务

1
2
3
4
5
6
7
8
use Illuminate\Foundation\Testing\DatabaseTransactions;

class UserTest extends TestCase
{
use DatabaseTransactions;

// 每个测试在事务中执行,自动回滚
}

6.2 模型工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use App\Models\User;

class UserTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

// 每个测试前执行
}

protected function tearDown(): void
{
// 每个测试后执行

parent::tearDown();
}
}

6.3 环境隔离

1
2
3
4
5
6
7
8
9
10
11
// tests/TestCase.php
abstract class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();

// 设置测试环境
config(['app.env' => 'testing']);
}
}

7. 实战案例

7.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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<?php

namespace Tests\Feature;

use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
use RefreshDatabase;

protected User $user;

protected function setUp(): void
{
parent::setUp();

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

public function test_guest_cannot_create_post(): void
{
$response = $this->postJson('/api/posts', [
'title' => 'Test Post',
'content' => 'Test Content',
]);

$response->assertUnauthorized();
}

public function test_authenticated_user_can_create_post(): void
{
$response = $this->actingAs($this->user)
->postJson('/api/posts', [
'title' => 'Test Post',
'content' => 'Test Content',
]);

$response->assertCreated()
->assertJsonPath('data.title', 'Test Post')
->assertJsonPath('data.user_id', $this->user->id);

$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'user_id' => $this->user->id,
]);
}

public function test_user_can_list_own_posts(): void
{
$posts = Post::factory()->count(5)->for($this->user)->create();

$response = $this->actingAs($this->user)
->getJson('/api/posts');

$response->assertOk()
->assertJsonCount(5, 'data');
}

public function test_user_can_update_own_post(): void
{
$post = Post::factory()->for($this->user)->create();

$response = $this->actingAs($this->user)
->putJson("/api/posts/{$post->id}", [
'title' => 'Updated Title',
]);

$response->assertOk()
->assertJsonPath('data.title', 'Updated Title');
}

public function test_user_cannot_update_others_post(): void
{
$otherUser = User::factory()->create();
$post = Post::factory()->for($otherUser)->create();

$response = $this->actingAs($this->user)
->putJson("/api/posts/{$post->id}", [
'title' => 'Updated Title',
]);

$response->assertForbidden();
}

public function test_user_can_delete_own_post(): void
{
$post = Post::factory()->for($this->user)->create();

$response = $this->actingAs($this->user)
->deleteJson("/api/posts/{$post->id}");

$response->assertNoContent();

$this->assertDatabaseMissing('posts', [
'id' => $post->id,
]);
}
}

7.2 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
// tests/Feature/PostTest.php
use App\Models\User;
use App\Models\Post;
use function Pest\Laravel\{actingAs, get, post, put, delete};

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

it('guest cannot create post', function () {
post('/api/posts', ['title' => 'Test'])
->assertUnauthorized();
});

it('authenticated user can create post', function () {
actingAs($this->user)
->post('/api/posts', [
'title' => 'Test Post',
'content' => 'Test Content',
])
->assertCreated()
->assertJsonPath('data.title', 'Test Post');

expect(Post::count())->toBe(1);
});

it('user can list own posts', function () {
Post::factory()->count(5)->for($this->user)->create();

actingAs($this->user)
->get('/api/posts')
->assertOk()
->assertJsonCount(5, 'data');
});

8. 最佳实践

8.1 测试命名

1
2
3
4
5
// 推荐:描述性命名
public function test_user_can_create_post_when_authenticated(): void {}

// 不推荐
public function testCreate(): void {}

8.2 测试组织

1
2
3
4
5
6
7
8
9
tests/
├── Feature/
│ ├── Auth/
│ ├── Posts/
│ └── Users/
├── Unit/
│ ├── Models/
│ └── Services/
└── TestCase.php

8.3 测试覆盖率

1
php artisan test --coverage

9. 总结

Laravel 13 的测试改进为开发者提供了更好的测试体验:

  1. 并行测试:显著提升测试速度
  2. Pest 支持:更优雅的测试语法
  3. 增强断言:更多语义化方法
  4. 测试隔离:更好的并行支持

通过本指南,您已经掌握了 Laravel 13 测试改进的核心内容,可以构建更完善的测试套件了。

参考资料