阻止不稳定测试:在 Laravel 测试中冻结时间

发布: (2026年1月10日 GMT+8 02:34)
3 min read
原文: Dev.to

Source: Dev.to

停止不稳定测试:在 Laravel 测试中冻结时间的封面图片

问题

我在本机上运行的这个测试本来很顺利,但在 CI 上会随机失败:

public function test_order_item_cancel(): void
{
    $user = UserFixture::createUser();
    $this->actingAsFrontendUser($user);

    $order = OrderFixture::create($user);
    $orderItem = OrderItemFactory::new()->for($order)->for($user)->create();

    $response = $this->put(route('api-v2:order.order-items.cancel', ['uuid' => $orderItem->uuid]));

    $response->assertNoContent();

    $this->assertDatabaseHas(OrderItem::class, [
        'uuid' => $orderItem->uuid,
        'canceled_at' => Date::now(),
    ]);
}

有时测试会出现以下错误:

Failed asserting that a row in the table [order_items] matches the attributes {
    "canceled_at": "2026-01-09T10:24:52.008406Z"
}.

Found: [
    {
        "canceled_at": "2026-01-09 12:24:51"
    }
].

起初我只是重新运行,但在阅读了 The Flaky Test Chronicles VI 后,我意识到需要调查这到底是实际的 bug 还是不稳定的测试。

为什么会这样

Date::now() 被调用了两次:

  1. 控制器设置 canceled_at 时。
  2. 测试检查该值时。

即使相差毫秒,时间戳也会不相等。CI 环境通常更慢,所以这种差异在 CI 上更常出现。

解决方案

在发起请求之前冻结时间,这样控制器和测试使用相同的时间戳。

// Option 1
$this->freezeTime();

// Option 2
$now = Date::now();
Date::setTestNow($now);

$response = $this->put(route('api-v2:order.order-items.cancel', ['uuid' => $orderItem->uuid]));

$this->assertDatabaseHas(OrderItem::class, [
    'uuid' => $orderItem->uuid,
    'canceled_at' => $now,
]);

$this->freezeTime()Date::setTestNow() 的便利包装,作用域限定在测试生命周期内。时间被冻结后,时间戳相匹配,测试变得确定性。

另一种方式

如果你只需要确保该字段不为空,可以断言 canceled_at 不是 null

$this->assertDatabaseMissing(OrderItem::class, [
    'uuid' => $orderItem->uuid,
    'canceled_at' => null,
]);

最后思考

当测试依赖时间时,要对时间进行控制。如果测试在本地通过但在 CI 失败,使用 Date::setTestNow()$this->freezeTime() 冻结时间。让你的测试具备确定性,才能保持可靠和可信。

Back to Blog

相关文章

阅读更多 »