Testing external API integrations thoroughly is one of the hardest parts of building reliable web applications. Live API calls are slow, flaky, costly, and can fail unpredictably due to network issues, rate limits, or provider outages. For these reasons, most mature test suites do not make real HTTP requests — they mock them.
This article walks you through mocking external HTTP APIs in plain PHP, from basics to advanced techniques, with sandbox-style examples you can run immediately. We’ll cover:
- Why mocking is essential
- Creating a PHP API client
- Mocking responses with Guzzle
- Dynamic and conditional fake responses
- Simulating error conditions (timeouts, server errors)
- Asserting requests were made correctly
- Sequential responses for testing retries
- Using sandbox APIs for integration
Previous article in testing category: https://codecraftdiary.com/2026/01/03/testing-database-logic-what-to-test-what-to-skip-and-why-it-matters/
1. Why Mock External API Calls
Before diving into code, here’s why mocking is crucial:
a. Speed
Live HTTP calls dramatically slow down tests — often orders of magnitude slower than in-memory logic.
b. Stability
Third-party APIs can be unreliable, returning downtime or throttling, which makes tests flaky.
c. Isolation
Tests should verify your application logic, not the availability or correctness of external services.
d. Cost and Limits
Calling paid APIs every test run can incur costs or hit rate limits.
2. Creating a PHP HTTP Client
We’ll use Guzzle as our HTTP client. Here’s a simple WeatherClient:
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class WeatherClient
{
private Client $client;
private string $baseUrl;
private string $apiKey;
public function __construct(string $baseUrl, string $apiKey)
{
$this->baseUrl = $baseUrl;
$this->apiKey = $apiKey;
$this->client = new Client(['base_uri' => $baseUrl]);
}
public function getCurrent(string $city): array
{
try {
$response = $this->client->request('GET', '/current', [
'query' => ['q' => $city, 'key' => $this->apiKey],
'http_errors' => false
]);
return json_decode($response->getBody()->getContents(), true);
} catch (RequestException $e) {
throw new \RuntimeException("API request failed: ".$e->getMessage());
}
}
}
- GuzzleHttp\Client handles HTTP requests
- Works in plain PHP without Laravel
3. Mocking API Responses with Guzzle
Guzzle provides MockHandler to simulate responses. Here’s a basic test:
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
// Mock API response
$mock = new MockHandler([
new Response(200, [], json_encode([
'location' => ['name' => 'London'],
'current' => ['temp_c' => 18]
]))
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack, 'base_uri' => 'https://api.weather.com']);
// Inject mock client into WeatherClient
$weatherClient = new WeatherClient('https://api.weather.com', 'dummy-key');
$ref = new ReflectionClass($weatherClient);
$prop = $ref->getProperty('client');
$prop->setAccessible(true);
$prop->setValue($weatherClient, $client);
// Make request (uses mocked response)
$result = $weatherClient->getCurrent('London');
echo "City: ".$result['location']['name']."\n"; // London
echo "Temperature: ".$result['current']['temp_c']."°C\n"; // 18
- No real HTTP requests are made
- Ideal for unit testing
3a. Basic Example: Mocking a Simple API in Plain PHP Sandbox
This version shows how to simulate external API responses in plain PHP, without Guzzle or Laravel, making it suitable for online sandbox environments like https://onlinephp.io/c/d4940
<?php
// Simple WeatherClient that returns fake responses
class WeatherClient
{
private $baseUrl;
private $apiKey;
public function __construct(string $baseUrl, string $apiKey)
{
$this->baseUrl = $baseUrl;
$this->apiKey = $apiKey;
}
// Fake API call returning predefined data
public function getCurrent(string $city): array
{
$fakeApiResponses = [
'London' => [
'location' => ['name' => 'London'],
'current' => ['temp_c' => 18, 'condition' => 'Partly cloudy']
],
'New York' => [
'location' => ['name' => 'New York'],
'current' => ['temp_c' => 22, 'condition' => 'Sunny']
]
];
return $fakeApiResponses[$city] ?? [
'location' => ['name' => $city],
'current' => ['temp_c' => null, 'condition' => 'Unknown']
];
}
}
// --- Sandbox Test ---
$client = new WeatherClient('https://api.fakeweather.com', 'dummy-key');
$cities = ['London', 'New York', 'Paris'];
foreach ($cities as $city) {
$result = $client->getCurrent($city);
$temp = $result['current']['temp_c'] ?? 'unknown';
$condition = $result['current']['condition'] ?? 'unknown';
echo "City: {$result['location']['name']}, Temperature: {$temp}°C, Condition: {$condition}\n";
}
Why include this:
Shows a fully runnable example in any PHP environment.
Illustrates the concept of mocking/faking API responses without external dependencies.
Complements the Guzzle-based example for readers who want a quick sandbox-ready approach.
4. Dynamic and Conditional Fake Responses
You can customize responses based on input:
$mock = new MockHandler([
function ($request, $options) {
parse_str($request->getUri()->getQuery(), $query);
if ($query['q'] === 'US') {
return new Response(200, [], json_encode(['data'=>'US result']));
}
return new Response(200, [], json_encode(['data'=>'Other result']));
}
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
- Use closures to decide response dynamically
5. Simulating Errors: Timeouts & Server Failures
a. Failed Connection / Timeout
$mock = new MockHandler([
new \GuzzleHttp\Exception\ConnectException(
"Connection failed",
new \GuzzleHttp\Psr7\Request('GET', '/current')
)
]);
b. Server Errors (HTTP 500)
$mock = new MockHandler([
new Response(500, [], 'Internal Server Error')
]);
c. Sequential Responses for Retry Logic
$mock = new MockHandler([
new Response(500, [], 'Fail'), // First request
new Response(200, [], json_encode(['location'=>['name'=>'London'], 'current'=>['temp_c'=>20]])), // Second
]);
- Useful for testing retry behavior
6. Assertions
In PHPUnit, you can assert the results:
$this->assertEquals('London', $result['location']['name']);
$this->assertEquals(18, $result['current']['temp_c']);
In a sandbox without PHPUnit, simple checks:
if ($result['location']['name'] === 'London') {
echo "Test passed!\n";
} else {
echo "Test failed!\n";
}
7. Using Fixtures for Realistic Responses
Instead of hardcoding JSON:
$body = file_get_contents('weather_fixture.json');
$mock = new MockHandler([ new Response(200, [], $body) ]);
- Easier to maintain and mirrors real API structure
8. Integration Testing with Sandbox APIs
Some APIs provide sandbox environments (Stripe, PayPal, GitHub) where you can test real HTTP requests safely.
For full sandbox testing:
- Configure your client’s base URL to the sandbox
- Optionally, use Wiremock or similar mock servers for advanced stubbing
- Run integration tests against sandbox to ensure real API compatibility
9. Best Practices
- Mock external APIs only, don’t mock internal logic
- Use descriptive fixtures for clarity
- Include error scenarios in tests (timeouts, 500 errors)
- Combine unit tests (mocked) with occasional sandbox integration tests
Top comments (0)