불안정한 테스트 방지: Laravel 테스트에서 시간 고정
Source: Dev.to

문제
내 로컬에서는 이 테스트가 정상적으로 실행됐지만, 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를 읽고 나서 이것이 실제 버그인지 플레이키 테스트인지 조사해야 한다는 것을 깨달았습니다.
왜 이런 일이 발생하나요
Date::now()가 두 번 호출됩니다:
- 컨트롤러가
canceled_at을 설정할 때. - 테스트가 값을 확인할 때.
밀리초 차이만 있어도 타임스탬프가 일치하지 않게 됩니다. 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()을 사용해 시간을 고정하십시오. 테스트를 결정론적으로 만들면 신뢰성과 안정성을 유지할 수 있습니다.