---
title: "Testing — HTTP Client Rate Limits for Laravel | Roundly"
description: "Swap in RateLimits::fake() to assert deferred, allowed and reset limits without real sleeps or Redis, or fake Laravel’s Sleep."
url: https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[HTTP Client Rate Limits for Laravel](https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel.md)

# Testing

RateLimits::fake() swaps the manager for a recording fake, so your suite can assert on throttling without real sleeps or Redis:

```php
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

$fake = RateLimits::fake();

Http::rateLimit(1, by: 'acct-1')->get('https://api.example.com/one');
Http::rateLimit(1, by: 'acct-1')->get('https://api.example.com/two');

$fake->assertDeferred('acct-1');   // a request for this key was throttled
$fake->assertAllowed();            // at least one request went through
// $fake->assertNothingDeferred(); // would fail here

RateLimits::perMinute(60)->by('stripe')->reset();
$fake->assertReset('stripe');
```

fake() returns a RateLimitsFake — a RateLimitManager subtype swapped into the container, so the facade, an injected manager and Http::rateLimit() all get it. The limiter still runs: hits are counted and waits computed, but nothing sleeps and no Redis is touched. Requests are still sent, so pair it with Http::fake(). The fake records the events of the limits it builds directly, not through the event dispatcher, so Event::fake() — before or after — cannot hide them.

## Every assertion

| Method | Checks / returns |
| --- | --- |
| `assertDeferred(?string $key = null)` | A request was deferred — for that owner key, when given. |
| `assertNothingDeferred()` | No request was deferred. |
| `assertAllowed(?string $key = null)` | A request was allowed — for that owner key, when given. |
| `assertNothingAllowed()` | No request was allowed. |
| `assertReset(?string $key = null)` | A limit was reset — for that owner key, when given. |
| `assertNothingReset()` | No limit was reset. |
| `deferredCount() / allowedCount()` | How many requests were deferred / allowed. |
| `store()` | The shared InMemoryStore; read hits with the store key, e.g. hits('acct-1:minute'). |
| `deferrer()` | The RecordingDeferrer: defers() and deferCount(). |

```php
// Every assertion returns the fake, so they chain.
$fake->assertDeferred();                // some request was deferred
$fake->assertDeferred('acct-1');        // … for this owner key
$fake->assertNothingDeferred();         // no request was deferred

$fake->assertAllowed();                 // some request was allowed
$fake->assertAllowed('acct-1');         // … for this owner key
$fake->assertNothingAllowed();          // no request went through

$fake->assertReset();                   // some limit was reset
$fake->assertReset('stripe');           // … for this owner key
$fake->assertNothingReset();            // nothing was reset

$fake->deferredCount();                 // int
$fake->allowedCount();                  // int
$fake->store()->hits('acct-1:minute');  // hits recorded for a key + window
$fake->deferrer()->defers();            // every wait in ms, e.g. [60000]
$fake->deferrer()->deferCount();        // int
```

## A complete test

```php
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

it('throttles the second call for the same account', function () {
    Http::fake(['*' => Http::response('ok')]);

    $fake = RateLimits::fake();

    Http::rateLimit(1, by: 'acct-1')->get('https://api.example.com/one');
    Http::rateLimit(1, by: 'acct-1')->get('https://api.example.com/two');

    $fake->assertAllowed('acct-1')->assertDeferred('acct-1');

    expect($fake->deferredCount())->toBe(1)
        ->and($fake->allowedCount())->toBe(2)
        ->and($fake->deferrer()->defers())->toBe([60_000]); // one full minute
});
```

- Every limit built while the fake is active shares one InMemoryStore and one RecordingDeferrer — so separate Http::rateLimit() calls with the same key and window share a budget.
- RateLimits::usingStore(), usingDeferrer() and releasingJob() still take effect under the fake: they return a manager on your override that keeps the fake’s recording store or deferrer for the other half — so a released job really is released, and its events are still recorded by the fake.
- The RecordingDeferrer never sleeps: it records each wait (defers(), deferCount()) and advances its own clock, so windows still progress deterministically. It also follows Carbon test time, so travel() past a window lets the next request straight through.
- Events are forced on while faking, regardless of events\_enabled, and the fake listens to them too — so a limit built before the swap is recorded as well (each event once).
- Combine with Http::fake() so no real request leaves the test.

## Faking Sleep instead

To keep the real limiter but skip real waiting, fake Laravel’s Sleep — the default SleepDeferrer pauses through it:

```php
use Illuminate\Support\Sleep;
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

Sleep::fake();

$limit = RateLimits::perSecond(1);

Http::withMiddleware($limit)->get('https://api.example.com/one');
Http::withMiddleware($limit)->get('https://api.example.com/two');

Sleep::assertSleptTimes(1); // the SleepDeferrer paused once, without really sleeping
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel.md)
