首页
/ Coolify 测试体系实践:Laravel 测试六大最佳规则与源码级落地指南

Coolify 测试体系实践:Laravel 测试六大最佳规则与源码级落地指南

2026-09-04 11:34:21作者:龚格成

本文以 Coolify 仓库中的 Laravel 测试最佳实践规则 testing.md 为主体,逐条拆解 LazilyRefreshDatabase、模型断言、Factory 状态与序列、Exceptions::fake()Event::fake() 时序与 recycle() 六条规则的原理与正确用法,并结合 Coolify 真实的 phpunit.xmltests/Pest.php 配置和测试用例(521 个 Feature 测试、298 个 Unit 测试)佐证其落地方式。读完本文,你能够在这套 PaaS 项目(以及任何大型 Laravel 应用)中写出更快、更隔离、更可维护的测试。

规则定位:Laravel Best Practices 技能包中的测试章节

testing.md 属于仓库内 .agents/skills/laravel-best-practices/ 技能包的一部分。该技能包的总入口 SKILL.md 将 20 个领域的规则按影响力排序,其中第 8 节「Testing Patterns」恰好对应本文的六条规则:

  • LazilyRefreshDatabase 优于 RefreshDatabase(更快)
  • assertModelExists() 优于裸的 assertDatabaseHas()
  • Factory 状态与序列优于手动覆盖字段
  • 善用各种 fake(Event::fake()Exceptions::fake() 等)——但永远放在 Factory 构建之后,而不是之前
  • recycle() 在多个 Factory 之间共享同一关系实例

技能包在应用任何规则前都强调「Consistency First(一致性优先)」:先查看同级文件里已有的模式,遵循现有代码库的约定,即使理论上存在更优的写法也不引入第二种风格。这些规则是「尚无既有模式时的默认值,而不是推翻现有约定的指令」。

Coolify 的测试基座:先看清隔离是怎么做的

理解这些规则之前,先看 Coolify 的测试环境如何保证隔离与速度。

PHPUnit 配置:全内存驱动、强制隔离

phpunit.xml 定义了 UnitFeaturev4 三个测试套件,并通过 <env> 强制锁定一批关键驱动:

<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array" force="true"/>
<env name="DB_CONNECTION" value="testing" force="true"/>
<env name="MAIL_MAILER" value="array" force="true"/>
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
<env name="SESSION_DRIVER" value="array" force="true"/>
<env name="BROADCAST_DRIVER" value="null" force="true"/>

几个关键取舍值得注意:

  • DB_CONNECTION=testing 指向 config/database.php 中第 98 行定义的 testing 连接,测试与开发/生产库物理隔离;
  • BCRYPT_ROUNDS=4 大幅降低密码哈希成本——Feature 测试中大量创建用户,这是显著提速手段;
  • 缓存、会话、邮件、广播全部走 array/null 驱动,队列走 sync 同步执行,让请求在单次进程内完成、结果可断言;
  • <source> 只统计 ./app 目录覆盖率,测试代码不计入。

这套「驱动全部内存化 + 同步执行」的配置,正是后文各种 fake() 规则的物理基础:既然没有真实的外部依赖,断言就应当聚焦在应用行为本身。

Pest 全局钩子:保证每个测试拿到全新状态

Coolify 以 Pest 为主要测试框架,tests/Pest.phpTestCase 绑定到 Featurev4/Featurev4/Browserv5/Browser 四个目录,并在 beforeEach 中执行:

beforeEach(function () {
    // Flush the Once memoization cache to ensure tests get fresh data
    Once::flush();
    // Flush the Server identity map cache to ensure tests get fresh data
    Server::flushIdentityMap();
    config(['broadcasting.default' => 'null']);
});

Once::flush() 清空 Laravel 的 memoization 缓存,Server::flushIdentityMap() 清空模型标识映射——这两行说明:跨请求的内存态缓存在测试间是最大隐患之一,不刷新就会出现「上一个测试的脏数据污染下一个测试」。这也呼应了规则 5 的核心思想:fake 与缓存的启用/禁用顺序必须与模型事件、工厂逻辑的正确协作顺序一致,顺序错了就会产生损坏的模型或错误的断言目标。

此外 tests/Pest.php 还定义了浏览器测试辅助函数 loginAndSkipBoarding(),以及 tests/TestCase.php(引入 CreatesApplication trait)作为所有测试类的基座。

规则一:用 LazilyRefreshDatabase 替代 RefreshDatabase

规则原文指出:

RefreshDatabase migrates once per process and wraps each test in a rolled-back transaction. LazilyRefreshDatabase skips even that first migration if the schema is already up to date.

RefreshDatabase 的行为是「每进程迁移一次 + 每个测试包在回滚事务里」;而 LazilyRefreshDatabase 在此基础上更进一步:如果 schema 已经是最新的,连首次迁移都直接跳过。在 Coolify 这种拥有 200 多个迁移文件(见 database/migrations)的项目里,跳过一次全量迁移对 CI 首测的提速是立竿见影的。

从源码结构看,Coolify 当前测试代码中普遍引入的是 RefreshDatabase trait。例如 tests/Feature/AdminSubscriptionStatusTest.php 顶部:

use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

这正体现了技能包的「Consistency First」:既有代码库尚未统一切换到 LazilyRefreshDatabase,所以该规则对现存 500 多个 Feature 测试而言是「重构方向」,对新写的测试则是推荐起点。

规则二:用模型断言替代裸数据库断言

规则给出的对照:

// Incorrect
$this->assertDatabaseHas('users', ['id' => $user->id]);

// Correct
$this->assertModelExists($user);

assertModelExists() 更表达化、类型安全,失败时给出的信息也更清晰——它直接以模型为断言对象,而不是把「表名 + 字段哈希」当作字符串约定。

Coolify 的测试代码里两种写法并存,这恰好能看出演进轨迹。较新的 API 测试已在用模型断言,例如 tests/Feature/Api/DestinationsApiTest.php 第 329 行:

$this->assertModelExists($this->destination);

而部分早期测试仍保留裸断言,例如 tests/Feature/Api/CloudProviderTokenApiTest.php 中的:

$this->assertDatabaseHas('cloud_provider_tokens', [
    // ...
]);

按规则二的原则,新代码应当一律使用 assertModelExists() / assertModelMissing();对既有文件则遵循「一致性优先」,在同一文件内保持既有风格即可。

规则三:使用 Factory 状态与序列

规则强调:

Named states make tests self-documenting. Sequences eliminate repetitive setup.

对照示例:

// Incorrect
User::factory()->create(['email_verified_at' => null]);

// Correct
User::factory()->unverified()->create();

具名状态让测试自我解释,序列则消除重复的构造代码。

Coolify 的 database/factories/UserFactory.php 中恰好就实现了规则示例里用到的 unverified 状态(第 31 行):

public function unverified(): static

这意味着 User::factory()->unverified()->create() 在 Coolify 里是可直接运行的写法。仓库内还有 ServerFactoryProjectFactoryEnvironmentFactoryApplicationFactory 等一批 Factory,编写新测试时应先在这些文件里复用或扩展具名状态,而不是在测试里手工堆 create([...]) 覆盖字段。

规则四:用 Exceptions::fake() 断言异常上报

规则指出:与其使用 withoutExceptionHandling(),不如用 Exceptions::fake()——它能在请求正常走完的前提下,断言「正确的异常被上报(report)了」,而不会让异常中断请求流程。

两者的差别在于断言目标不同:

手段 行为 适用场景
withoutExceptionHandling() 关闭异常处理器,让异常直接抛出到测试 验证「异常确实被抛出」
Exceptions::fake() 用 fake 接管 report 通道,请求按正常错误响应流程走完 验证「异常被上报且可渲染」

对 Coolify 这种带完整 app/Exceptions/Handler.php、以及 DeploymentExceptionProcessExceptionRateLimitException 等自定义异常的 PaaS 应用,Exceptions::fake() 更适合写「某部署失败时系统上报了 DeploymentException」这类断言:既验证了上报路径,又不破坏请求的正常返回流程。

规则五:Event::fake() 必须在 Factory 构建之后调用

这是六条规则中唯一涉及调用时序的一条,规则原文:

Model factories rely on model events (e.g., creating to generate UUIDs). Calling Event::fake() before factory calls silences those events, producing broken models.

对照:

// Incorrect
Event::fake(); $user = User::factory()->create();

// Correct
$user = User::factory()->create(); Event::fake();

原因很直接:模型工厂依赖 creatingcreated 等模型事件来完成初始化逻辑(Coolify 的模型大量使用 UUID 作为主键,这类赋值常挂在 creating 事件上)。Event::fake() 一旦在工厂创建之前生效,这些事件就被静默掉,产生的模型会带着缺失的字段,污染后续断言。

Coolify 测试中已有 Event::fake() 的实际使用,如 tests/Unit/ServerBackoffTest.phptests/Feature/Livewire/ConfigurationCheckerTest.php。使用时的纪律就是:先完成所有 Factory 数据准备,再启用 fake,然后把断言集中在 fake 作用域内。这与 tests/Pest.phpbeforeEach 统一刷新全局缓存的思路同源——把「状态准备」与「行为断言」两个阶段干净地分开。

规则六:用 recycle() 跨 Factory 共享同一关系实例

规则原文:

Without recycle(), nested factories create separate instances of the same conceptual entity.

Ticket::factory()
    ->recycle(Airline::factory()->create())
    ->create();

不用 recycle() 时,嵌套工厂会为「概念上同一个实体」各自创建独立实例(比如父记录和子记录指向两个不同的 Airline),导致外键关系断裂或断言目标错位。recycle() 让多个工厂复用同一个已创建的实例,保证父子关系指向同一行数据。

映射到 Coolify 的领域模型,这是高频需求:一个 Team 下挂 ProjectServerApplication 等多层资源。测试多资源关联场景时,先用工厂显式创建父级实体并 recycle() 进子工厂,比让嵌套工厂各造各的更可控。

应用这些规则的实操清单

把六条规则整合成在 Coolify 中新增/重构测试时的检查清单:

  1. 数据库 trait:新测试优先评估 LazilyRefreshDatabase;修改既有文件时跟随文件现状(Consistency First);
  2. 存在性断言:一律 assertModelExists() / assertModelMissing(),避免裸 assertDatabaseHas()
  3. 数据构造:先查 database/factories 里已有的具名状态(如 unverified()),缺失就补状态而不是在测试里覆盖字段;
  4. 异常断言:验证上报用 Exceptions::fake(),保留请求正常流程;
  5. fake 时序:所有 Factory 数据准备完成后,再调 Event::fake()
  6. 关系共享:多工厂指向同一概念实体时用 recycle()
  7. 环境前提:确保测试在 phpunit.xml 定义的 testing 数据库连接与全内存驱动下运行,必要时在 beforeEach 中刷新 Once、模型标识映射等全局缓存,保证测试间零串扰。

最后重申 SKILL.md 的总原则:这些规则解决的是「代码库里还没有既定模式时怎么写」的问题。面对具体文件,先读同级测试文件——一致性永远优先于理论最优。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384