Tech Verse Logo
Enable dark mode
Mocking HTTP Clients in Laravel 12 with Http::fake

Mocking HTTP Clients in Laravel 12 with Http::fake

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

3 min read

Preventing Stray Outbound Network Calls

If you don't call Http::preventStrayRequests() inside your base test setup, your test suite is a time bomb. Without it, any un-mocked HTTP call made by your application will silently hit the live internet. That leads to flaky builds on GitHub Actions, random rate-limiting penalties, and polluted third-party environments.

Add this call inside the setUp method of your base TestCase class or inside a test trait:

protected function setUp(): void
{
    parent::setUp();

    \Illuminate\Support\Facades\Http::preventStrayRequests();
}

With stray requests disabled, Laravel throws a RuntimeException the moment your application attempts an HTTP call that doesn't match an explicit fake rule. It forces you to be explicit about every outbound integration in your system.

Pattern Matching and Faking Responses

The Http::fake() method accepts an array where keys represent URL patterns and values represent the stubbed response. Wildcards (*) let you match entire domains or specific route paths.

When matching endpoints, order matters. Laravel evaluates patterns sequentially from top to bottom. Place specific endpoint rules before generic wildcard rules, or your wildcard will swallow requests intended for more specific stubs.

use Illuminate\Support\Facades\Http;

Http::fake([
    'api.stripe.com/v1/charges' => Http::response(['id' => 'ch_123', 'status' => 'succeeded'], 200),
    'api.stripe.com/*' => Http::response(['error' => 'not_found'], 404),
    '*' => Http::response(['message' => 'Service Unavailable'], 503),
]);

If you need dynamic logic based on request attributes, pass a closure as the value in your mapping array. The closure receives an instance of Illuminate\Http\Client\Request and must return an Illuminate\Http\Client\Response.

Inspecting Dynamic Payloads in Fakes

Http::fake([
    'api.github.com/repos/*' => function (\Illuminate\Http\Client\Request $request) {
        if ($request->hasHeader('X-GitHub-Api-Version', '2022-11-28')) {
            return Http::response(['name' => 'laravel/framework'], 200);
        }

        return Http::response(['message' => 'Unsupported API version'], 400);
    },
]);

Simulating Errors and Retries with Sequences

Testing happy paths is easy, but testing network instability and API rate limits is where Http::sequence() proves its worth. Applications running on PHP 8.3 often rely on Laravel's Http::retry() functionality to gracefully handle temporary failures. You can simulate sequential status codes to ensure your retry logic behaves as intended.

Here is an integration test asserting that a service retries on 500 errors and succeeds when the API recovers on the third attempt:

use Illuminate\Support\Facades\Http;
use App\Services\PaymentGateway;

public function test_payment_retry_succeeds_after_transient_failures(): void
{
    Http::fake([
        'api.payment-provider.com/v1/transact' => Http::sequence()
            ->push(['error' => 'Server Error'], 500)
            ->push(['error' => 'Rate Limit Exceeded'], 429)
            ->push(['transaction_id' => 'tx_99812'], 200),
    ]);

    $gateway = new PaymentGateway();
    $response = $gateway->charge(1000, 'usd');

    $this->assertEquals('tx_99812', $response->id);
    Http::assertSentCount(3);
}

By default, if a sequence receives more requests than it has configured responses, it throws an OutOfBoundsException. If you want any subsequent requests to return a default fallback response instead of failing, use the whenEmpty() method on the sequence object.

Asserting Outgoing Request Payload and Headers

Returning a fake response isn't enough. You must verify that your service sent the correct payload, authentication headers, and query parameters. Use Http::assertSent() with an inspection callback.

use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

public function test_webhook_dispatches_correct_payload_and_signature(): void
{
    Http::fake([
        'hooks.slack.com/*' => Http::response([], 200),
    ]);

    // Trigger code that fires the request
    $this->postJson('/api/orders', ['amount' => 4999]);

    Http::assertSent(function (Request $request) {
        return $request->url() === 'https://hooks.slack.com/services/test/token'
            && $request->method() === 'POST'
            && $request->hasHeader('Authorization', 'Bearer secret-token')
            && $request['text'] === 'New order received: $49.99'
            && $request->isJson();
    });
}

The callback passed to Http::assertSent() must evaluate to boolean true for the assertion to pass. Array access on the $request object provides access to decoded JSON request bodies, saving you from decoding raw strings with json_decode($request->body(), true) manually.

Inspecting Query Parameters and Form Data

When working with GET parameters or multi-part form submissions, basic URL equality checks will fail if query parameter orders vary. Instead, parse parameters using the helper methods provided on the request instance.

Http::assertSent(function (Request $request) {
    return $request->hasHeader('Accept', 'application/json')
        && $request->data()['filter']['status'] === 'active'
        && $request->query()['page'] === '2';
});

Handling Asynchronous Requests and Request Pools

Laravel allows executing concurrent HTTP calls using Http::pool(). Faking pools relies on the same underlying Http::fake() setup, but verifying them requires checking individual asynchronous endpoints separately or using Http::assertSentInOrder() if execution sequencing matters.

To guarantee that your test suite doesn't miss unintended extra network traffic, end critical integration tests with Http::assertSentCount($expectedCount) or verify that un-triggered services remain silent using Http::assertNotSent().

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Preventing Laravel Cache Stampedes with Atomic Locks

    Preventing Laravel Cache Stampedes with Atomic Locks

    Fast Laravel Database Testing Without In-Memory SQLite

    Fast Laravel Database Testing Without In-Memory SQLite

    Testing Laravel APIs with Pest: A Practical Guide

    Testing Laravel APIs with Pest: A Practical Guide

    Mocking HTTP Clients in Laravel 12 with Http::fake

    Mocking HTTP Clients in Laravel 12 with Http::fake

    Detecting and Fixing N+1 Queries in Laravel 12

    Detecting and Fixing N+1 Queries in Laravel 12

    Model Caching Patterns in Laravel 12 for Production

    Model Caching Patterns in Laravel 12 for Production

    Ensuring Secure URLs in Laravel Applications

    Ensuring Secure URLs in Laravel Applications

    Simple Feature Flags in Laravel with Laravel Toggle

    Simple Feature Flags in Laravel with Laravel Toggle

    Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

    Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

    Laravel diffForHumans() Guide: Display Human-Readable Time Like a Pro

    Laravel diffForHumans() Guide: Display Human-Readable Time Like a Pro