引言
单元测试验证「函数对不对」,但真正保住系统的是特性测试——它把「一个 HTTP 请求进来 → 经过路由/中间件/控制器/服务 → 返回响应」整条链路跑一遍。Laravel 为特性测试提供了一流的支持:RefreshDatabase、HTTP 断言、契约测试、假事件。本文讲清 Laravel 里怎么写有效的特性测试,以及如何用 TDD 驱动日常开发。
前置:/php-testing-practice/(PHPUnit/Pest 基础)、/php-laravel-internals/(Laravel 内核)、/php-laravel-eloquent-advanced/(模型层)。
目录
- 1. 测试金字塔在 Laravel 的落地
- 2. Feature Test 基础:HTTP 测试与断言
- 3. RefreshDatabase 与数据库测试
- 4. 工厂 Factory:构造测试数据
- 5. 假事件、假队列与 Mock
- 6. 契约测试:跨服务接口保障
- 7. 并行测试与提速
- 8. TDD 工作流:红绿重构
- 9. 覆盖率与 CI 集成
- 10. 速查表与一句话记忆
- 延伸阅读
1. 测试金字塔在 Laravel 的落地
1.1 金字塔结构
/ 特性测试(少而全:走整条链路)
/ 单元测试(多而快:函数/类级)
/ —— 大量:纯函数、模型、服务
1.2 Laravel 的测试目录
tests/
├── Unit/ # 单元测试(纯函数、类)
├── Feature/ # 特性测试(HTTP、DB、整链路)
└── TestCase.php
记忆:Laravel 测试金字塔——单元测试多而快、特性测试少而全(整条 HTTP 链路);tests/Unit 与 tests/Feature 分置。
2. Feature Test 基础:HTTP 测试与断言
2.1 一个最小特性测试
// tests/Feature/PostApiTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostApiTest extends TestCase
{
use RefreshDatabase;
public function test_create_post_returns_201()
{
$response = $this->postJson('/api/posts', [
'title' => 'Hello',
'body' => 'World',
], ['Authorization' => 'Bearer ' . $token]);
$response->assertStatus(201)
->assertJson(['title' => 'Hello'])
->assertJsonPath('data.user_id', 1);
}
}
2.2 常用断言
| 断言 | 作用 |
|---|---|
| assertStatus / assertOk | 状态码 |
| assertJson / assertJsonPath | JSON 结构 |
| assertSessionHasErrors | 表单错误 |
| assertDatabaseHas / assertDatabaseMissing | 数据库状态 |
| assertRedirect | 重定向 |
记忆:特性测试 = postJson/getJson + 状态码/JSON/数据库断言;RefreshDatabase 让每次测试在干净库上跑。
3. RefreshDatabase 与数据库测试
3.1 两种策略
| 策略 | 做法 | 适合 |
|---|---|---|
| RefreshDatabase | 每测试跑迁移 | 开发/CI 环境 |
| DatabaseTransactions | 用事务回滚 | 已有大库/慢迁移 |
use Illuminate\Foundation\Testing\DatabaseTransactions;
class OrderTest extends TestCase { use DatabaseTransactions; }
3.2 环境隔离
Laravel 默认用 phpunit.xml 的 DB_DATABASE 测试库,确保不碰生产数据:
<env name="DB_DATABASE" value=":memory:"/> <!-- 或独立测试库 -->
<env name="DB_CONNECTION" value="sqlite"/>
记忆:RefreshDatabase 每次跑迁移最干净、DatabaseTransactions 用事务回滚适合大库;测试库用独立 DB,绝不碰生产数据。
4. 工厂 Factory:构造测试数据
4.1 定义工厂
// database/factories/PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraph(),
'user_id' => User::factory(),
];
}
}
4.2 使用
Post::factory()->count(10)->create(); // 10 篇
Post::factory()->for($user)->published()->create(); // 关联 + 状态
4.3 状态(State)
public function published() { return $this->state(['status' => 'published']); }
记忆:Factory 定义字段(fake() 生成)、状态用 state()、create() 落库、make() 不落库——测试数据的标准生产线。
5. 假事件、假队列与 Mock
5.1 假事件:不真正派发
Event::fake(); // 拦截所有事件
$this->postJson('/api/posts', [...]);
Event::assertDispatched(PostCreated::class);
Event::assertNotDispatched(SendWelcomeEmail::class);
5.2 假队列
Queue::fake();
// ...
Queue::assertPushed(SendNotificationJob::class, fn ($job) => $job->userId === 1);
5.3 Mock 外部依赖
$gateway = Mockery::mock(PaymentGateway::class);
$gateway->shouldReceive('charge')->once()->andReturn('txn_123');
$this->app->instance(PaymentGateway::class, $gateway);
记忆:Event::fake 拦截事件并断言派发、Queue::fake 断言任务入队、Mockery 替身外部服务——特性测试把副作用挡在门内。
6. 契约测试:跨服务接口保障
6.1 什么是契约测试
服务 A 和服务 B 约定一个接口格式,两端各自独立测试这个约定——A 不依赖 B 在跑,也能验证「我发的 B 一定接得住」。
6.2 Laravel + 契约测试
// 用契约库(如 Pact)
use PactPhp\Pact;
// Provider 侧:验证自己满足契约
$pact = Pact::provider('order-service');
$pact->given('订单存在')
->uponReceiving('查询订单请求')
->willRespondWith(status: 200, body: ['id' => 1, 'status' => 'paid'])
->verify();
// Consumer 侧:按契约 mock 对方
$pact = Pact::consumer('bff');
$pact->given(...)->willReturn([...]);
6.3 价值
✓ 两端独立演进
✓ 集成问题提前在 CI 暴露
✓ 减少联调成本
记忆:契约测试 = 两端各自验证同一接口约定(Pact);consumer mock 对方、provider 自证响应——CI 里提前发现契约破坏,减少联调。
7. 并行测试与提速
7.1 并行测试
# Laravel 10+ 内置并行
php artisan test --parallel
# 需要 MySQL/Postgres 支持,sqlite :memory: 需变体
// phpunit.xml 开启
<phpunit ... parallel="true">
7.2 提速技巧
// 复用已迁移库(测试间共享 schema)
use Illuminate\Foundation\Testing\TestsWithGlobalMigrations; // 自定义 trait
// 减少建数据:能 make() 不 create()
// 只测需要覆盖的断言
记忆:php artisan test –parallel 并行跑;提速三板斧——复用 schema、少落库(make 优先)、精简断言。
8. TDD 工作流:红绿重构
8.1 三阶段
红:先写失败的测试(描述期望行为)
绿:写最少代码让测试通过
重构:在不破坏绿的前提下整理代码
8.2 一个 TDD 示例
// 第 1 步(红):先写特性测试
public function test_duplicate_email_rejected()
{
User::factory()->create(['email' => 'a@b.com']);
$resp = $this->postJson('/api/users', ['email' => 'a@b.com']);
$resp->assertStatus(422);
}
// 第 3 步(重构):加唯一校验
// 控制器/请求里加 'email' => 'required|email|unique:users,email'
记忆:TDD 红绿重构——先写失败的测试定义行为、写最少代码变绿、安全重构;特性测试是「系统行为」最好的规格。
9. 覆盖率与 CI 集成
9.1 生成覆盖率
./vendor/bin/phpunit --coverage-text --coverage-html build/coverage
# Pest: ./vendor/bin/pest --coverage
9.2 门禁与重点
✓ 核心业务(订单/支付)要求高覆盖率
✓ 覆盖率 < 阈值(如 80%)CI 失败
✓ 只看增量覆盖:新代码必须被测试
# CI
- name: Run tests
run: ./vendor/bin/pest --coverage --min=80
记忆:覆盖率监控核心业务、–min=80 设 CI 门禁;增量覆盖优先——新代码必须带测试,别为数字注水。
10. 速查表与一句话记忆
| 场景 | 做法 |
|---|---|
| HTTP 测试 | postJson/getJson + assertJsonPath |
| 数据库 | RefreshDatabase / DatabaseTransactions |
| 数据构造 | Factory + state() |
| 副作用隔离 | Event::fake / Queue::fake / Mock |
| 跨服务 | 契约测试(Pact) |
| 提速 | –parallel + make() 优先 |
| 流程 | TDD 红绿重构 |
| 门禁 | –coverage –min=80 |
一句话记忆:Laravel 特性测试 = 用 Feature Test 走通「HTTP 请求→中间件→控制器→DB→响应」整条链路(postJson + assertStatus/JsonPath/DatabaseHas);RefreshDatabase 干净库、Factory 构造数据、Event::fake/Queue::fake/Mock 隔离副作用;跨服务用契约测试(Pact)两端自证;php artisan test –parallel 并行提速;按 TDD 红绿重构写行为规格;CI 用 –coverage –min=80 设门禁、核心业务保覆盖率。
延伸阅读
- /php-testing-practice/ — PHPUnit/Pest 基础与 Mock 策略
- /php-laravel-internals/ — Laravel 请求生命周期
- /php-laravel-eloquent-advanced/ — 模型与数据库测试
- /php-api-design-rest/ — API 契约设计
- /php-static-analysis-quality/ — 静态分析与质量门禁
- [[testing]] — 跨语言测试方法论
- Laravel 测试文档
- Pest 文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。