Stop Flaky Tests: Freeze Time in Laravel Testing

Published: (January 9, 2026 at 01:34 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Cover image for Stop Flaky Tests: Freeze Time in Laravel Testing

The Problem

I had this test running smoothly on my machine, but on CI it failed randomly:

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(),
    ]);
}

Sometimes the test produced this error:

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"
    }
].

At first I just retried, but after reading The Flaky Test Chronicles VI I realized I needed to investigate whether this was a real bug or a flaky test.

Why This Happens

Date::now() is called twice:

  1. When the controller sets canceled_at.
  2. When the test checks the value.

Even a millisecond difference makes the timestamps unequal. CI environments are often slower, so the discrepancy appears more frequently there.

The Fix

Freeze time before making the request so both the controller and the test use the same timestamp.

// 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() is a convenient wrapper around Date::setTestNow() that is scoped to the test lifecycle. With the time frozen, the timestamps match and the test becomes deterministic.

Another Way

If you only need to ensure the field is not empty, you can assert that canceled_at is not null:

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

Final Thoughts

When tests depend on time, take control of it. If a test passes locally but fails in CI, freeze time using Date::setTestNow() or $this->freezeTime(). Making your tests deterministic will keep them reliable and trustworthy.

Back to Blog

Related posts

Read more »

Day 1 - class-booking-system Project

Day 1: Starting a Laravel Class Booking System What I Did Today I decided to build a class booking system with Laravel. I haven't used Laravel in production be...