Using Carbon::setTestNow() to make time sensitive assertions in Laravel and PHPUnit

Published: 24.01.22 Last Updated: 25.01.22

In some tests, assertions on time can become a problem, let's take a look at the following examples and see how Carbon::setTestNow() can solve potential time issues in assertions showing up.

Example 1: A file is being saved with a date-time stamp appended in the filename

// BEFORE

/** @test */
public function a_spreadsheet_is_exported_to_s3()
{
    Order::factory()->count(3)->create();

    /* 
     * Let's assume that on a slower machine i.e Github Actions this could 
     * take 1 second. You could simulate this in your tests with sleep(1); 
     * in order to force a failure for *demonstration* purposes.
     */
    OrderExporterJob::dispatch(); 

    /* 
     * When that one-second delay is introduced via a slower machine or 
     * manually via a sleep() call. Then the following assertion will fail.
     */
    $this->assertTrue(Storage::disk('s3')->exists(
        'exports/orders-' . Carbon::now()->format('Y-m-d H:i:s') . '.csv', 
    ));
}
// AFTER

/** @test */
public function a_spreadsheet_is_exported_to_s3()
{
    /* 
     * Carbon::setTestNow() forces the timestamp to be the same for the 
     * duration of the test.
     */
    Carbon::setTestNow(Carbon::now()); 
    Order::factory()->count(3)->create();

    OrderExporterJob::dispatch();
    sleep(1);

    /* 
     * Even with the hardcoded sleep() call present, the test will pass.
     * NOTE: The sleep() is here for demonstration purposes only.
     */
    $this->assertTrue(Storage::disk('s3')->exists(
        'exports/orders-' . Carbon::now()->format('Y-m-d H:i:s') . '.csv',
    ));
}

Example 2: A temporary signed URL is being used in a test assertion

// BEFORE

/** @test */
public function guests_can_edit_their_information()
{
    $guest = Guest::factory()->create();

    $response = $this->get(
        URL::temporarySignedRoute(
            'guests.edit', 
            Carbon::now()->addDays(7), [
                'contact' => $guest->uuid,
            ]
        )
    );

    /*
     * Similarly as per the previous example, we want to make an assertion on 
     * a time-sensitive value. Here, if the time were to change by a second 
     * then our assertion on the guestUpdateRoute then we would have an invalid 
     * signature. Using Carbon::setTestNow(Carbon::today()); will ensure the 
     * tests pass even if on a slow machine.
     */
    $response->assertOk()
        ->assertPropValue('guest', $guest)
        ->assertPropValue('guestUpdateRoute', URL::temporarySignedRoute(
            'guests.update', Carbon::now()->addDays(7), [
                'guest' => $guest->uuid,
            ]
        ));
}
// AFTER

/** @test */
public function guests_can_edit_their_information()
{
    /*
     * Using Carbon::setTestNow(Carbon::today()); will ensure the tests 
     * pass even if on a slow machine.
     */
    Carbon::setTestNow(Carbon::today());
    $guest = Guest::factory()->create();

    $response = $this->get(
        URL::temporarySignedRoute(
            'guests.edit', 
            Carbon::now()->addDays(7), [
                'contact' => $guest->uuid,
            ]
        )
    );
    sleep(1);

    /* 
     * Even with the hardcoded sleep() call present, the test will pass.
     * NOTE: The sleep() is here for demonstration purposes only.
     */
    $response->assertOk()
        ->assertPropValue('guest', $guest)
        ->assertPropValue('guestUpdateRoute', URL::temporarySignedRoute(
            'guests.update', Carbon::now()->addDays(7), [
                'guest' => $guest->uuid,
            ]
        ));
}

Have questions or want to stay up to date? Find me on Twitter Get notified of new posts