laravelmastery
SnippetsArticlesSeriesNews

Test-driven Laravel · Lesson 3 of 3

Test-driven Laravel · Lesson 3

Freezing time

Edges and judgementTesting

The PDF test kept the file off the real disk. A test that asserts issued_on, sent_at or an expiry will fail when it runs a second later than the code, or across midnight. freezeTime() makes now() return one instant for the whole test.

Video

01 Pin the clock, then move it when the date matters

Freeze at the start, before the code writes a timestamp. travel() is for the case where an hour has to pass.

PHP
it('expires the payment link after an hour', function () {
    $this->freezeTime();

    $invoice = Invoice::factory()->issued()->create();

    $this->actingAs($invoice->team->owner)
        ->post('/invoices/'.$invoice->id.'/link');

    $this->travel(61)->minutes();

    $this->get('/pay/'.$invoice->fresh()->pay_token)
        ->assertForbidden();
});

The snippet on freezing time shows freezeTime(), travel() and travelTo() on their own, with a password reset token.

Freeze time in tests.

Call travel() only in the test that needs the clock to move. A frozen clock in every test hides a bug that depends on time passing.

02 Assert a timestamp only after the clock is pinned

sent_at is written with now(). Without freezeTime(), the assertion has to allow a range, and a range hides a bug that writes yesterday. Freeze, act, then compare the column to the frozen instant.

PHP
it('records when the invoice was sent', function () {
    $this->freezeTime();

    $invoice = Invoice::factory()->issued()->create();

    $this->actingAs($invoice->team->owner)
        ->post('/invoices/'.$invoice->id.'/send')
        ->assertRedirect();

    expect($invoice->fresh()->sent_at->equalTo(now()))->toBeTrue();
});

travel() comes after that post when the test is about expiry, as in the link above. It does not belong in this one. The sent_at test wants a single instant, and now() inside the expectation is that same instant because the clock is still frozen.

freezeTime() also freezes Carbon inside the factory. An issued_on of now() in the factory is the same instant as the assertion. If the test needs a date in the past, pass that date in the factory and leave the clock frozen. travel() before the post would make sent_at match the travelled time, and the test would still pass for the wrong reason.

Recap

You can pin now() for one test, and move it only when the behaviour is an expiry.

  • Freeze before the write.
  • travel() is for the test where an hour has to pass. It does not belong on a sent_at equality.
  • A frozen clock in every test hides a bug that depends on time passing.

Check yourself · 3 questions

Question 1 of 3sent_at is compared to now() without freezeTime(). The test flakes. What do you add?

now() is one instant, so the timestamp assertion can be exact. Next, run one body against each bad status.

← PreviousFaking storage and HTTP

On this page

  • Pin the clock, then move it when the date matters
  • Assert a timestamp only after the clock is pinned
  • Recap
  • Check yourself

Test-driven Laravel

  • 1 Overview

The shape of a test

  • 2 What a feature test is for
  • 3 Arrange, act, assert
  • 4 Refreshing the database
  • 5 Factories that read like the domain
  • 6 Acting as a user

The paths that matter

  • 7 HTTP tests for happy paths
  • 8 Validation errors
  • 9 Authorisation

Edges and judgement

  • 10 Faking the mail
  • 11 Faking the queue
  • 12 Faking storage and HTTP
  • 13 Freezing time
  • 14 Datasets
  • 15 What not to test

Test-driven Laravel

Lessons

  1. 01Overview

The shape of a test

  1. 02What a feature test is for
  2. 03Arrange, act, assert
  3. 04Refreshing the database
  4. 05Factories that read like the domain
  5. 06Acting as a user

The paths that matter

  1. 07HTTP tests for happy paths
  2. 08Validation errors
  3. 09Authorisation

Edges and judgement

  1. 10Faking the mail
  2. 11Faking the queue
  3. 12Faking storage and HTTP
  4. 13Freezing time
  5. 14Datasets
  6. 15What not to test

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.