
Laravel 功能测试中,若在模型工厂中显式赋值 created_at、updated_at 或 deleted_at,会干扰 Eloquent 的自动时间戳管理机制,导致内存数据库中记录虽存在但路由模型绑定失败。本文详解问题成因与规范修复方式。
laravel 功能测试中,若在模型工厂中显式赋值 created_at、updated_at 或 deleted_at,会干扰 eloquent 的自动时间戳管理机制,导致内存数据库中记录虽存在但路由模型绑定失败。本文详解问题成因与规范修复方式。
在 Laravel 功能测试中,使用模型工厂(如 Team::factory()->create())创建记录后,模型看似已成功写入内存数据库(如 SQLite in-memory),但在后续 HTTP 请求(如 deleteJson)中却抛出 ModelNotFoundException:“No query results for model [App\Models\Team]...”,而同一测试上下文中直接调用 Team::find($id) 却能正常返回实例——这种“存在却不可见”的矛盾现象,往往源于工厂中对时间戳字段的不当手动赋值。
? 问题根源:工厂覆盖了 Eloquent 时间戳自动行为
Eloquent 默认会在模型创建/更新时自动填充 created_at、updated_at 字段,并在软删除时设置 deleted_at。当工厂中显式指定这些字段(尤其是 deleted_at)时,会破坏以下关键机制:
-
软删除兼容性:若工厂中写死
'deleted_at' => Carbon::now(),该记录将被标记为已软删除,而默认的路由模型绑定(Route::model('team', Team::class))不会自动应用withTrashed(),因此Team::findOrFail($id)在绑定阶段即失败; -
内存数据库事务隔离:测试中启用
RefreshDatabase特征时,每个测试在独立事务中运行;手动设置时间戳可能导致时间精度、时区或序列化差异,在某些驱动(如 SQLite in-memory)中引发隐式不一致; -
模型状态不一致:
$team->exists为true,但其deleted_at值使->trashed()返回true,而框架路由解析器未感知此状态,直接按“活跃记录”查找,自然无结果。
典型错误工厂代码如下:
// ❌ 错误示例:手动设置时间戳(尤其 deleted_at)
class TeamFactory extends Factory
{
protected $model = Team::class;
public function definition(): array
{
return [
'id' => Str::uuid(),
'name' => $this->faker->company(),
'deleted_at' => Carbon::now(), // ⚠️ 导致软删除,路由绑定失败
'created_at' => Carbon::now(), // ⚠️ 覆盖 Eloquent 自动行为
'updated_at' => Carbon::now(), // ⚠️ 同上
];
}
}✅ 正确做法:交由 Eloquent 自动管理时间戳
应完全移除工厂中对 created_at、updated_at、deleted_at 的手动赋值,让 Eloquent 在 save() 时自动处理:
// ✅ 正确示例:仅定义业务字段,时间戳交由框架管理
class TeamFactory extends Factory
{
protected $model = Team::class;
public function definition(): array
{
return [
'id' => Str::uuid(),
'name' => $this->faker->company(),
// ✅ 删除 deleted_at / created_at / updated_at 行
];
}
}若需测试软删除场景,应在测试中显式调用 ->delete() 或 ->forceDelete(),而非在工厂中预设状态:
// ✅ 在测试中可控地触发软删除
public function testSoftDeletedTeamIsNotResolvedByRoute()
{
$team = Team::factory()->create();
$team->delete(); // 真实触发软删除
// 此时路由绑定会失败(符合预期),如需访问需 withTrashed()
$this->getJson(route('teams.show', $team))
->assertNotFound();
// 显式启用软删除查询
$this->getJson(route('teams.show', $team) . '?with_trashed=1')
->assertOk();
}?️ 额外建议:增强测试健壮性
-
验证工厂行为:在测试中添加断言确认时间戳是否自动生成:
$team = Team::factory()->create(); $this->assertNotNull($team->created_at); $this->assertNotNull($team->updated_at); $this->assertNull($team->deleted_at); // 确保未被意外软删除
-
检查模型配置:确认
Team模型正确启用了时间戳和软删除:class Team extends Model { use HasFactory, SoftDeletes; // ✅ 必须引入 SoftDeletes trait public $timestamps = true; // ✅ 默认为 true,无需显式设 } -
避免 UUID 主键陷阱:若使用
Str::uuid()作为主键,请确保数据库字段类型为char(36)或uuid,且迁移中已禁用自动递增:Schema::create('teams', function (Blueprint $table) { $table->uuid('id')->primary(); // ✅ 使用 uuid 类型 $table->string('name'); $table->timestamps(); $table->softDeletes(); // ✅ 显式声明软删除字段 });
遵循以上原则,即可彻底解决“工厂创建了记录,但测试中路由找不到”的诡异问题,确保测试环境与生产行为严格一致。


















