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

    Local LLM Development with Ollama and Hardware Limits

    Local LLM Development with Ollama and Hardware Limits

    LLM API Pricing Comparison for Side Projects

    LLM API Pricing Comparison for Side Projects

    Building Semantic Search with Laravel 12 and Next.js 16

    Building Semantic Search with Laravel 12 and Next.js 16

    Fine-Tuning vs RAG vs Prompts: Choosing the Right AI Tool

    Fine-Tuning vs RAG vs Prompts: Choosing the Right AI Tool

    Wiring LLMs with Tool Function Calling

    Wiring LLMs with Tool Function Calling

    LLM API Rate Limit Caching and Quota Guarding

    LLM API Rate Limit Caching and Quota Guarding

    Streaming LLM Responses with Laravel 12 and Next.js 16

    Streaming LLM Responses with Laravel 12 and Next.js 16

    Pgvector Similarity Search in Production Postgres

    Pgvector Similarity Search in Production Postgres

    React Drag and Drop with dnd-kit and Laravel

    React Drag and Drop with dnd-kit and Laravel

    Building a Production RAG Pipeline in PHP 8.3

    Building a Production RAG Pipeline in PHP 8.3