skip to content

In Laravel 13, how do Sleep::fake() and Str::freezeUuids() make a refund retry's back-off and generated reference testable?

level: middleimportance: nice to knowfreq 22%

answer

  1. Sleep::for() wraps sleep/usleep
  2. fake records durations, no pause
  3. assertSequence, assertNeverSlept
  4. freezeUuids, createUuidsUsingSequence
  5. 13 resets Str factories at teardown

basics

~10 s

Sleep::fake() makes Laravel's Sleep class record durations instead of pausing, so back-off can be asserted with Sleep::assertSequence(). Str::freezeUuids() makes Str::uuid() return one known value; Laravel 13 resets these Str factories after each test.

solid answer

~30 s

Laravel's `Sleep` class wraps PHP's `sleep()`/`usleep()` (`Sleep::for(2)->seconds()`). After `Sleep::fake()`, a sleep **records** its duration and returns at once, so a retry loop runs instantly; you assert with `Sleep::assertSequence([...])`, `Sleep::assertSleptTimes(3)`, or `Sleep::assertNeverSlept()`. `Sleep::fake(syncWithCarbon: true)` also advances Carbon's clock by each fake sleep. Calls to native `sleep()` are not intercepted. For identifiers, `Str::freezeUuids()` returns the frozen UUID and makes every `Str::uuid()` return it; `Str::createUuidsUsingSequence([...])`, `Str::freezeUlids()` and `Str::createRandomStringsUsing()` do the same for sequences, ULIDs and random strings. In Laravel 13 the test case resets these `Str` factories at teardown, so tests that relied on one set in an earlier test must set it again.

code

php · 21 lines
php
<?php

use App\Jobs\ProcessRefund;
use App\Models\Refund;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;

test('refund retries with growing back-off and a stable reference', function () {
    Sleep::fake();
    $reference = Str::freezeUuids();

    Http::fake([
        'payments.test/*' => Http::sequence()->pushStatus(503)->pushStatus(503)->push(['status' => 'succeeded']),
    ]);

    (new ProcessRefund(Refund::factory()->create()))->handle();

    Sleep::assertSequence([Sleep::for(1)->second(), Sleep::for(2)->seconds()]);
    Http::assertSent(fn ($r) => $r['reference'] === (string) $reference);
});

go deeper

for a junior

Recall that Sleep::fake() stops real pauses in tests and Str::freezeUuids() makes every Str::uuid() call return one known value.

for a middle

Explain that only calls through Sleep and Str are faked, name the assertion methods, and note syncWithCarbon for moving the clock.

for a senior

Refactor native sleep() and ad hoc ID generation behind Sleep and Str, and adjust suites for Laravel 13's per-test Str factory reset.

for a principal

Set a codebase rule that time, pauses and randomness go through fakeable seams, so deterministic tests are the default rather than a retrofit.

## Two sources of non-determinism A refund job that retries a payment call typically **waits** between attempts (back-off), and a refund record often gets a generated **reference** such as a UUID. Both make tests slow or unpredictable. Laravel provides fakes for each through classes you call statically. ## `Sleep`: a testable wrapper around pausing `Illuminate\Support\Sleep` is a small wrapper around PHP's native `sleep()` and `usleep()` with a readable API: ```php Sleep::for(2)->seconds(); Sleep::for(500)->milliseconds(); Sleep::for(1)->second()->and(10)->milliseconds(); ``` In production it pauses. After `Sleep::fake()` it instead **records the duration** and returns immediately. Assertions: | Assertion | Checks | |---|---| | `Sleep::assertSequence([Sleep::for(1)->second(), Sleep::for(2)->seconds()])` | the exact order of sleeps | | `Sleep::assertSleptTimes(3)` | how many sleeps happened | | `Sleep::assertSlept(fn (CarbonInterval $d) => ..., times: 2)` | a matching sleep occurred a given number of times | | `Sleep::assertNeverSlept()` | no sleep was recorded at all | | `Sleep::assertInsomniac()` | every recorded sleep had a zero duration | `Sleep::fake(syncWithCarbon: true)` additionally moves Carbon's test time forward by each fake sleep, so code that compares `now()` before and after a wait sees the time pass. `Sleep::whenFakingSleep(fn ($duration) => ...)` runs a callback on every fake sleep. The important limit: only code that goes through `Sleep` is faked. A retry loop calling PHP's `sleep(2)` directly still pauses the test. Laravel's own `retry()` helper, which the HTTP client's `retry()` builds on, already pauses through `Sleep`, so its back-off is covered; hand-written loops should use `Sleep::for()` too. The base test case turns the sleep fake off during teardown, so it does not carry into the next test. ## `Str` factories: predictable identifiers `Str::uuid()`, `Str::orderedUuid()`, `Str::ulid()` and `Str::random()` normally produce fresh random values. Test helpers let you control them: 1. `Str::freezeUuids()` generates one UUID, makes every later `Str::uuid()` return it, and returns it to you. With a closure, the freeze lasts only for the closure. 2. `Str::createUuidsUsing(fn () => Uuid::fromString('...'))` installs a custom factory. 3. `Str::createUuidsUsingSequence([...])` returns values in order, with an optional fallback once the list runs out. 4. `Str::freezeUlids()`, `Str::createUlidsUsing()`, `Str::createRandomStringsUsing()` and `Str::createRandomStringsUsingSequence()` do the same for ULIDs and random strings. 5. `Str::createUuidsNormally()` and its siblings restore random generation. This lets a test assert the exact reference on the refund, in the mail and in the payment request payload. ## The Laravel 13 change The Laravel 13 upgrade guide lists a low-impact change: custom `Str` factories are **reset during test teardown**. The base test case's state flush now calls `Str::resetFactoryState()`, which restores normal UUID, ULID and random-string generation. A suite that froze UUIDs once in an early test and silently relied on that in later tests will see fresh random values; set the factory in each test or in `setUp()`. ## Why not mock these instead? `Sleep` and `Str` are called statically and are not facades, so there is no container binding to swap and no `shouldReceive()` to call. The fake switches are built into the classes themselves, which is also why they are global to the PHP process until something resets them. That global scope is exactly what the Laravel 13 teardown reset addresses for `Str`, and what the base test case's `Sleep::fake(false)` at teardown addresses for `Sleep`. ## Putting it together - Put back-off through `Sleep::for()` in the job, not native `sleep()`. - In the test: `Sleep::fake()`, run the job against a faked provider that fails twice, then `Sleep::assertSequence()` on the waits. - Freeze the reference with `Str::freezeUuids()` and assert it appears in the outgoing request and the stored refund. - Rely on teardown to reset both fakes; do not rely on state from earlier tests.

  • After upgrading to Laravel 13, tests that expected a frozen UUID set in an earlier test start seeing random values. Why?
    Laravel 13's test teardown calls `Str::resetFactoryState()`, restoring normal UUID, ULID and random-string generation after every test. Earlier versions let a custom factory persist between test methods. Call `Str::freezeUuids()` or `createUuidsUsing()` in each test or in `setUp()`.
  • A retry loop still slows the suite even with Sleep::fake() in place. What is the likely cause?
    The loop calls PHP's native `sleep()` or `usleep()` rather than `Sleep::for()`. The fake only affects calls made through Laravel's `Sleep` class, so rewrite the pause as `Sleep::for($seconds)->seconds()`.

saying these in an interview costs you the question

  • Sleep::fake() also stops PHP's native sleep() calls
  • Str::freezeUuids() in one test stays active for the rest of a Laravel 13 run
  • Sleep::fake() always advances Carbon's clock by the slept duration
  • Str::freezeUuids() returns nothing, so the test cannot know the frozen value