docs(zh-CN): sync Chinese docs with latest upstream changes

This commit is contained in:
neo
2026-03-21 12:55:58 +08:00
parent 0af0fbf40b
commit e73c2ffa34
85 changed files with 11028 additions and 747 deletions

View File

@@ -0,0 +1,283 @@
---
name: laravel-tdd
description: 使用 PHPUnit 和 Pest、工厂、数据库测试、模拟以及覆盖率目标进行 Laravel 的测试驱动开发。
origin: ECC
---
# Laravel TDD 工作流
使用 PHPUnit 和 Pest 为 Laravel 应用程序进行测试驱动开发,覆盖率(单元 + 功能)达到 80% 以上。
## 使用时机
* Laravel 中的新功能或端点
* 错误修复或重构
* 测试 Eloquent 模型、策略、作业和通知
* 除非项目已标准化使用 PHPUnit否则新测试首选 Pest
## 工作原理
### 红-绿-重构循环
1. 编写一个失败的测试
2. 实施最小更改以通过测试
3. 在保持测试通过的同时进行重构
### 测试层级
* **单元**:纯 PHP 类、值对象、服务
* **功能**HTTP 端点、身份验证、验证、策略
* **集成**:数据库 + 队列 + 外部边界
根据范围选择层级:
* 对纯业务逻辑和服务使用**单元**测试。
* 对 HTTP、身份验证、验证和响应结构使用**功能**测试。
* 当需要验证数据库/队列/外部服务组合时使用**集成**测试。
### 数据库策略
* 对于大多数功能/集成测试使用 `RefreshDatabase`(每次测试运行运行一次迁移,然后在支持时将每个测试包装在事务中;内存数据库可能每次测试重新迁移)
* 当模式已迁移且仅需要每次测试回滚时使用 `DatabaseTransactions`
* 当每次测试都需要完整迁移/刷新且可以承担其开销时使用 `DatabaseMigrations`
`RefreshDatabase` 作为触及数据库的测试的默认选择:对于支持事务的数据库,它每次测试运行运行一次迁移(通过静态标志)并将每个测试包装在事务中;对于 `:memory:` SQLite 或不支持事务的连接,它在每次测试前进行迁移。当模式已迁移且仅需要每次测试回滚时使用 `DatabaseTransactions`
### 测试框架选择
* 新测试默认使用 **Pest**(当可用时)。
* 仅在项目已标准化使用它或需要 PHPUnit 特定工具时使用 **PHPUnit**
## 示例
### PHPUnit 示例
```php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectControllerTest extends TestCase
{
use RefreshDatabase;
public function test_owner_can_create_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
$this->assertDatabaseHas('projects', ['name' => 'New Project']);
}
}
```
### 功能测试示例HTTP 层)
```php
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectIndexTest extends TestCase
{
use RefreshDatabase;
public function test_projects_index_returns_paginated_results(): void
{
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = $this->actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
}
}
```
### Pest 示例
```php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
uses(RefreshDatabase::class);
test('owner can create project', function () {
$user = User::factory()->create();
$response = actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
assertDatabaseHas('projects', ['name' => 'New Project']);
});
```
### Pest 功能测试示例HTTP 层)
```php
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
test('projects index returns paginated results', function () {
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
});
```
### 工厂和状态
* 使用工厂生成测试数据
* 为边缘情况定义状态(已归档、管理员、试用)
```php
$user = User::factory()->state(['role' => 'admin'])->create();
```
### 数据库测试
* 使用 `RefreshDatabase` 保持干净状态
* 保持测试隔离和确定性
* 优先使用 `assertDatabaseHas` 而非手动查询
### 持久性测试示例
```php
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectRepositoryTest extends TestCase
{
use RefreshDatabase;
public function test_project_can_be_retrieved_by_slug(): void
{
$project = Project::factory()->create(['slug' => 'alpha']);
$found = Project::query()->where('slug', 'alpha')->firstOrFail();
$this->assertSame($project->id, $found->id);
}
}
```
### 副作用模拟
* 作业使用 `Bus::fake()`
* 队列工作使用 `Queue::fake()`
* 通知使用 `Mail::fake()``Notification::fake()`
* 领域事件使用 `Event::fake()`
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
dispatch(new SendOrderConfirmation($order->id));
Queue::assertPushed(SendOrderConfirmation::class);
```
```php
use Illuminate\Support\Facades\Notification;
Notification::fake();
$user->notify(new InvoiceReady($invoice));
Notification::assertSentTo($user, InvoiceReady::class);
```
### 身份验证测试Sanctum
```php
use Laravel\Sanctum\Sanctum;
Sanctum::actingAs($user);
$response = $this->getJson('/api/projects');
$response->assertOk();
```
### HTTP 和外部服务
* 使用 `Http::fake()` 隔离外部 API
* 使用 `Http::assertSent()` 断言出站负载
### 覆盖率目标
* 对单元 + 功能测试强制执行 80% 以上的覆盖率
* 在 CI 中使用 `pcov``XDEBUG_MODE=coverage`
### 测试命令
* `php artisan test`
* `vendor/bin/phpunit`
* `vendor/bin/pest`
### 测试配置
* 使用 `phpunit.xml` 设置 `DB_CONNECTION=sqlite``DB_DATABASE=:memory:` 以进行快速测试
* 为测试保持独立的环境,以避免触及开发/生产数据
### 授权测试
```php
use Illuminate\Support\Facades\Gate;
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
```
### Inertia 功能测试
使用 Inertia.js 时,使用 Inertia 测试辅助函数来断言组件名称和属性。
```php
use App\Models\User;
use Inertia\Testing\AssertableInertia;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class DashboardInertiaTest extends TestCase
{
use RefreshDatabase;
public function test_dashboard_inertia_props(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('Dashboard')
->where('user.id', $user->id)
->has('projects')
);
}
}
```
优先使用 `assertInertia` 而非原始 JSON 断言,以保持测试与 Inertia 响应一致。