Why HTTP mocking matters in Laravel tests
When your Laravel application makes external API calls—whether to Stripe, Twilio, or a third-party weather service—those requests create friction in your test suite. Real HTTP calls are slow, flaky, depend on external services, and can incur costs. Mocking HTTP calls in Laravel using the built-in testing utilities lets you verify integration logic without leaving your machine. We'll walk through best practices for mocking HTTP calls in Laravel, covering everything from simple URL patterns to advanced assertion strategies that keep your tests reliable and maintainable.
Getting started: Http::fake with URL patterns
Laravel's HTTP testing facade provides Http::fake(), which intercepts all HTTP requests during tests and returns canned responses. The simplest approach is to fake a single URL.
use Illuminate\Support\Facades\Http;
public function test_fetches_user_from_external_api()
{
Http::fake([
'api.example.com/users/1' => Http::response(['id' => 1, 'name' => 'John'], 200),
]);
$response = Http::get('https://api.example.com/users/1');
$this->assertEquals('John', $response['name']);
}The Http::fake() array maps URL patterns to responses. This works for exact matches, but most real-world integrations need pattern matching. Use wildcards to handle dynamic segments.
Http::fake([
'api.example.com/users/*' => Http::response(['id' => 1, 'name' => 'John'], 200),
'api.example.com/posts/*/comments' => Http::response(['comments' => []], 200),
]);Patterns follow simple wildcard syntax where * matches any characters within a single path segment. This allows you to test multiple endpoints with one fake configuration. In production integrations, you'll often need to match several related endpoints—for instance, fetching a user, then their profile, then their posts. Wildcards keep your test setup DRY and readable.
You can also fake all requests with a default response, then override specific ones.
Http::fake(function ($request) {
return Http::response(['status' => 'ok'], 200);
});The callback receives the pending request object, giving you full control to inspect headers, method, or body before deciding what to return. This is powerful for testing conditional logic or request inspection within your application code.
Handling sequences: simulating retries and failure recovery
Real integrations often involve retry logic. Your code might need to handle transient failures, rate limits, or exponential backoff. Mocking a sequence of responses lets you test that path without waiting or polluting your test with complex mocking libraries.
public function test_retries_on_rate_limit()
{
Http::fake([
'api.example.com/data' => Http::sequence()
->push(Http::response(['error' => 'Rate limited'], 429), 429)
->push(Http::response(['error' => 'Rate limited'], 429), 429)
->push(Http::response(['data' => 'success'], 200), 200),
]);
$response = Http::retry(3, 100)->get('https://api.example.com/data');
$this->assertTrue($response->successful());
}The Http::sequence() method returns responses in order. Each call to the endpoint consumes the next item in the sequence. This is essential for testing retry logic—you can simulate two 429 responses followed by success, and verify your retry handler kicks in appropriately.
Sequences also handle final failures gracefully.
Http::fake([
'api.example.com/data' => Http::sequence()
->push(Http::response(null, 500))
->push(Http::response(null, 500))
->push(Http::response(null, 500))
->whenEmpty(Http::response(['error' => 'exhausted'], 500)),
]);
// After 3 attempts, whenEmpty kicks in
$response = Http::retry(5, 100)->get('https://api.example.com/data');The whenEmpty() method defines what happens when the sequence runs out. This prevents test failures from unexpected calls and makes your intent clearer: you're deliberately testing exhaustion of retries.
Asserting on sent requests
Mocking responses is only half the story. You also need to verify that your application sent the right request—correct URL, headers, authentication, and payload. The Http::assertSent() method lets you inspect requests after execution.
public function test_sends_correct_headers_to_payment_provider()
{
Http::fake();
Http::withHeaders(['Authorization' => 'Bearer secret-key'])
->post('https://api.stripe.com/charges', ['amount' => 1000]);
Http::assertSent(function ($request) {
return $request->url() === 'https://api.stripe.com/charges' &&
$request->header('Authorization') === 'Bearer secret-key' &&
$request['amount'] === 1000;
});
}The callback receives a request object (actually a PendingRequest) that you can inspect. You can check the URL, method, headers, query parameters, and body. This assertion pattern is crucial for verifying that your integration code constructs requests correctly.
For common patterns, Laravel provides shortcut assertions.
Http::assertSent(function ($request) {
return $request->hasHeader('Authorization') &&
$request->method() === 'POST' &&
$request->isJson();
});You can also assert that a request was not sent, or count how many times a request was sent.
Http::assertNotSent(function ($request) {
return str_contains($request->url(), 'deprecated');
});
Http::assertSentCount(3); // Exactly 3 requests sentIn a system that talks to multiple external services, you'll often want to verify not just that requests were sent, but that they were sent in the right order and with the right data. Assertions help catch bugs where your code skips a required API call or sends stale data.
Preventing stray requests and debugging test failures
A dangerous scenario in test suites: you configure Http::fake() for certain URLs, but your code accidentally calls a different endpoint. Without safeguards, that request might silently fail or hit the real internet. To catch these bugs early, prevent unmocked requests.
public function test_with_strict_request_checking()
{
Http::fake([
'api.example.com/users' => Http::response(['users' => []], 200),
]);
// This will throw if any unmocked URL is called
Http::preventStrayRequests();
$this->artisan('fetch:users')->assertSuccessful();
}Calling Http::preventStrayRequests() before your test logic ensures that if your code tries to call any URL not in the fake registry, the test will fail with a clear error. This is a safety net that catches typos, refactored endpoints, or logic errors that would otherwise slip through.
When debugging a failed assertion, the error message should point you to the problem. Laravel includes the request details in the failure output.
public function test_sends_user_id_correctly()
{
Http::fake();
Http::preventStrayRequests();
// Your application code
$service = new UserService();
$service->fetchUser(123);
// This assertion fails: the code sent user_id=124
Http::assertSent(function ($request) {
return $request['user_id'] === 123;
});
}The test output will show what requests were actually sent, helping you pinpoint the mismatch. Keep your assertions focused—each one should verify one behavior. Overly broad assertions hide bugs.
Common mistakes
Not mocking all endpoints: If your application calls three APIs but you only mock two, the third call will either fail silently or hit the real internet. Always use Http::preventStrayRequests() to catch this during test runs, not in production.
Mocking at the wrong level: Some teams mock at the HTTP layer (using Laravel's testing facades) while others mock at the service layer (replacing the service class). The HTTP layer is better for integration tests; service mocks are better for unit tests. Don't mix both in the same test without a clear reason.
Forgetting to assert on requests: Just because a response is mocked doesn't mean your code constructed the request correctly. Always verify the request URL, method, headers, and body using assertions. A test that only checks the mocked response is a false positive.
Brittle URL matching: Hardcoding exact URLs in your fake registry makes tests fragile. If you refactor a URL, tests break even though the logic didn't change. Use configuration or environment variables for base URLs, then construct patterns from them.
// Better: centralize the base URL
$baseUrl = config('services.external_api.url');
Http::fake([
"{$baseUrl}/users/*" => Http::response([...], 200),
]);Ignoring sequence exhaustion: When using Http::sequence(), if your code makes more requests than you've configured responses for, the test will hang or fail. Always set whenEmpty() or ensure your sequence has enough responses for the worst-case number of retries.
Key takeaways
- Use
Http::fake()with URL patterns to intercept HTTP calls; wildcards match dynamic segments like user IDs or post slugs. - Employ
Http::sequence()to simulate retry scenarios, transient failures, and rate limiting without real delays. - Always assert on sent requests using
Http::assertSent()to verify your application constructs correct payloads, headers, and URLs. - Call
Http::preventStrayRequests()in tests to catch unmocked endpoints early and prevent accidental real API calls. - Keep URL mocking patterns DRY by centralizing base URLs; avoid brittle hardcoded URLs in test fixtures.
- Match your mocking strategy to your test type: use HTTP mocks for integration tests, service mocks for unit tests.






Comments
Loading comments…