Laravel 特性测试实战:Feature Test、契约测试与 TDD 工作流

Laravel 特性测试实战:测试金字塔在 Laravel 的落地、Feature Test 与 HTTP 测试、数据库测试(RefreshDatabase/事务)、契约测试与契约驱动开发、Mock 与假事件、并行测试、TDD 工作流与重构实践、覆盖率与 CI 集成。

引言

单元测试验证「函数对不对」,但真正保住系统的是特性测试——它把「一个 HTTP 请求进来 → 经过路由/中间件/控制器/服务 → 返回响应」整条链路跑一遍。Laravel 为特性测试提供了一流的支持:RefreshDatabase、HTTP 断言、契约测试、假事件。本文讲清 Laravel 里怎么写有效的特性测试,以及如何用 TDD 驱动日常开发。

前置:/php-testing-practice/(PHPUnit/Pest 基础)、/php-laravel-internals/(Laravel 内核)、/php-laravel-eloquent-advanced/(模型层)。


目录


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 / assertJsonPathJSON 结构
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 文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 面向对象与设计模式:SOLID、常用模式与 Laravel 实践
  2. PHP 静态分析与代码质量:PHPStan、Psalm、Rector 与 CI 门禁
  3. PHP 部署运维实战:Nginx、PHP-FPM、Docker 与 CI/CD