9.1 竣工验收:测试体系


9.1 竣工验收:测试体系

本节摘要:测试是项目的竣工验收:Feature 测试模拟完整请求路径,工厂批量造数,数据库事务回滚保证每条用例互不污染,Mail、Notification、Queue、Event 的伪造(fake)让"该发生的发生了"变成可断言的事实。本节以工单系统的验收为案例,把从零写起、隔离手段、断言技巧一次走完,读完你能给核心路径配上可持续运行的回归保险。

验收要解决的两个心魔

写测试的阻力通常来自两个心魔。一是"没时间":上线日期压着,测试看着不能直接换钱。可一旦核心路径没有测试,每次改动都靠人肉回归,改三处要手点三十个页面——省下的时间加倍还了。二是"测了也没用":那多半是测试写在了错误的层面(比如把私有方法测了个遍,用户路径却裸奔)。本节的立场很明确:以 Feature 测试为主力,按用户路径组织用例,用最少的代码锁住最多的回归风险。

第一条验收用例

给"登记工单"写验收:一个登录用户提交合法表单,应该建单成功、看到跳转、触发通知。

<?php // tests/Feature/CreateOrderTest.php namespace Tests\Feature; use App\Models\Team; use App\Models\User; use App\Models\WorkOrder; use App\Notifications\OrderCreated; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Notification; use Tests\TestCase; class CreateOrderTest extends TestCase { use RefreshDatabase; // 每条用例跑在事务里,结束自动回滚 public function test_登录用户可以登记工单(): void { Notification::fake(); // 通知不真发,只记录 $team = Team::factory()->create(); $user = User::factory()->create(['team_id' => $team->id]); $response = $this->actingAs($user)->post('/orders', [ 'title' => '外墙翻新', 'budget' => 50000, 'due_at' => now()->addWeek()->toDateString(), ]); $response->assertRedirect(); // 成功后跳转 $this->assertDatabaseHas('work_orders', [ 'title' => '外墙翻新', 'team_id' => $team->id, ]); Notification::assertSentTo( $team, OrderCreated::class ); } public function test_标题超长时验证拦截并回显错误(): void { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/orders', [ 'title' => str_repeat('长', 101), 'budget' => 100, ]); $response->assertSessionHasErrors('title'); // 第四章的质检单在此验收 $this->assertDatabaseCount('work_orders', 0); } }

这条用例集合了四个关键机制。RefreshDatabase 把每条用例包在事务里,跑完回滚——用例之间互不污染,谁先谁后结果一致(第九章第一节就承诺的"隔离"靠它实现)。工厂造数只造用例需要的最小关联。actingAs 模拟登录,走的是真实的 session 认证路径。Notification::fake 拦下通知只记录不发信,断言"发了、发给谁、发了几次",速度快且结果确定。

图 9-1:验收金字塔与本项目的侧重

图 9-1:验收金字塔与本项目的侧重

隔离与伪造:让测试又快又确定

除了 RefreshDatabase 与 Notification::fake,常用的伪造还有一整套,各自堵住一种"测试时不想真干"的副作用:

<?php // 邮件伪造:断言发了什么 Mail::fake(); // ... Mail::assertSent(OrderShipped::class, fn ($m) => $m->order->id === $order->id); // 队列伪造:断言任务推了、推了几个 Queue::fake(); // ... Queue::assertPushed(SendOrderNotification::class, 1); // 事件伪造:断言广播了 Event::fake(); // ... Event::assertDispatched(OrderCompleted::class); // 时间伪造:过期逻辑的可控时刻 $this->travelTo(now()->subDays(40)); // ...构造"过期"现场 $this->travelBack(); // HTTP 层伪造:第三方支付回调不真打外网 Http::fake(['pay.example.com/*' => Http::response(['ok' => 1])]);

伪造的取舍原则:被测对象是"我们的编排逻辑"时,把外部依赖 fake 掉(发信、支付网关、队列);被测对象就是外部交互本身时,用真实的测试账套或沙箱,fake 反而把被测的东西测没了。数据库侧的另一种隔离方式是用内存 SQLite 换速度,代价是部分 MySQL 专有行为测不出来——核心业务库的用例,我的建议是保持与生产同款数据库,速度问题用并行执行(php artisan test --parallel)解决。

⚠️ 常见坑:测试里写 sleep 和依赖执行顺序的用例是维护噩梦——前者用时间伪造替代,后者用 setUp 独立构造现场替代;断言只写"结果在库里长什么样",不逐行校验中间过程,否则重构一次改一片用例,测试反而成了枷锁。

让测试进流水线

测试写完不自动跑等于没写。最低配置是把它挂进部署流程:构建阶段跑全量 php artisan test,红了一条就中止上线;再进一步,代码仓库的推送钩子上跑、合并请求前必须绿。Dusk 这类浏览器测试因为慢,放夜间定时任务即可,不必挡每次部署。

本节要点回顾

  • 主力是 Feature:按用户路径组织用例,一次锁住路由、验证、授权、写库整条链。
  • 隔离双支柱:RefreshDatabase 事务回滚加工厂最小造数,用例互不污染。
  • fake 有边界:编排逻辑用伪造,外部交互本身用沙箱实测。
  • 进流水线才算数:部署前全量跑,红灯即中止,浏览器测试放夜间。

验收过了,接下来在上线前做最后一件事:把速度提上去。下一节讲缓存与性能优化,哪些动作立竿见影、哪些是心理安慰。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U