guzzlehttp/test-server
Guzzle Test Server is a dev-only testing helper that starts a local Node.js HTTP server, letting PHP tests queue responses and inspect received requests for handler and integration tests.
Install Dependencies:
Ensure Node.js (^20.19 || ^22.13 || >=24) is available in your environment (CI/CD or local dev).
composer require guzzlehttp/test-server
Basic Test Case:
use GuzzleHttp\Server\Server;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Client;
beforeEach(function () {
Server::start();
register_shutdown_function([Server::class, 'stop']);
});
it('mocks a simple API response', function () {
Server::enqueue(new Response(200, [], '{"status": "success"}'));
$client = new Client();
$response = $client->request('GET', Server::$url);
expect($response->getStatusCode())->toBe(200);
expect((string) $response->getBody())->toBe('{"status": "success"}');
});
Key Entry Points:
Server::start(): Launches the Node.js server (auto-stops on shutdown).Server::enqueue(): Queues PSR-7 responses for incoming requests.Server::enqueueRawByteParts(): Queues verbatim-byte responses written in delayed parts (new in 1.1.0 — useful for streaming, SSE, and chunked-transfer scenarios).Server::$url: Dynamic URL for the test server (e.g., http://127.0.0.1:34567).Server::received(): Returns an array of received requests for validation.// Queue a static response
Server::enqueue(new Response(201, ['Location' => '/items/1'], 'Created'));
// Queue dynamic responses (e.g., sequential)
Server::enqueue([
new Response(200, [], 'First call'),
new Response(200, [], 'Second call'),
]);
$client->post(Server::$url, [
'json' => ['amount' => 100],
]);
$requests = Server::received();
expect($requests[0]->getMethod())->toBe('POST');
expect(json_decode($requests[0]->getBody(), true))->toEqual(['amount' => 100]);
Server::enqueue(new Response(200, [
'WWW-Authenticate' => 'Bearer',
'Content-Type' => 'application/json',
], '{"token": "mock-token"}'));
$client = new Client(['auth' => ['token', '']]);
$response = $client->get(Server::$url);
// Use with Laravel's HTTP client
$response = Http::withOptions(['verify' => false])->get(Server::$url);
// Or with Guzzle directly
$guzzle = new Client(['base_uri' => Server::$url]);
$response = $guzzle->get('/endpoint');
Server::start(['port' => 8080]); // Fixed port
Server::start(['host' => 'localhost', 'port' => 0]); // Random port
Server::enqueueRawByteParts() lets you queue a response whose body is written as a sequence of verbatim byte parts, flushed between each part. This is the right tool when you need to test consumers that read incrementally (HTTP streaming, SSE, chunked transfer, long polling).
Server::enqueueRawByteParts([
"data: first-event\n\n",
"data: second-event\n\n",
"data: [done]\n\n",
]);
$response = (new Client())->get(Server::$url);
$body = (string) $response->getBody();
// Each part is written to the wire exactly as provided — no JSON/PSR-7 encoding step.
Use this instead of Server::enqueue(new Response(...)) when:
Guzzle stream option, Http::withOptions(['stream' => true])).Use middleware to inspect requests and return dynamic responses:
Server::enqueue(function ($request) {
if ($request->getMethod() === 'POST') {
return new Response(201, [], 'Created');
}
return new Response(405, [], 'Method Not Allowed');
});
Simulate webhook payloads with headers:
Server::enqueue(new Response(200, [
'X-Signature' => 'mock-sig',
'Content-Type' => 'application/json',
], json_encode(['event' => 'payment.succeeded'])));
Mock 5xx errors or rate limits:
Server::enqueue(new Response(503, [], 'Service Unavailable'));
Combine enqueueRawByteParts() with a streaming client to exercise incremental parsing:
Server::enqueueRawByteParts([
"event: message\ndata: {\"n\":1}\n\n",
"event: message\ndata: {\"n\":2}\n\n",
]);
$response = Http::withOptions([
'stream' => true,
'timeout' => 2.0,
])->get(Server::$url);
$stream = $response->toPsrResponse()->getBody();
// Consume $stream incrementally and assert on each event
For performance, reuse the server across tests (e.g., in a TestCase setup):
class ApiTestCase extends TestCase {
protected static $server;
public static function setUpBeforeClass(): void {
self::$server = Server::start();
}
public static function tearDownAfterClass(): void {
self::$server->stop();
}
}
Node.js Dependency:
# In CI/CD or setup script
node --version || exit 1
Port Conflicts:
Server::start(['port' => 8080]);
Request Reconstruction:
Host) may not reconstruct correctly.>=0.4.0 (parses ports from Host headers).Windows Compatibility:
>=0.3.1 or ensure Node.js is installed via nvm.Shutdown Race Conditions:
register_shutdown_function or explicit try/catch:
try {
Server::start();
// Tests...
} finally {
Server::stop();
}
Raw Byte Parts Are Literal:
enqueueRawByteParts() writes bytes exactly as given. There is no Content-Length recalculation, no charset guessing, and no JSON coercion. A missing \n\n in an SSE frame, or a mismatched delimiter, silently produces a broken stream.No Automatic Headers on Raw Parts:
Response-style header defaults. If you need Content-Type: text/event-stream or Transfer-Encoding: chunked, assert on — or explicitly configure — the response headers rather than assuming the client will infer them.Response for the header-bearing request when needed, and keep enqueueRawByteParts() for the body-only streaming case.Streaming Client Timeouts:
timeout (e.g., ['timeout' => 2.0]) and/or read a fixed number of events before closing the stream.Inspect Received Requests:
dd(Server::received()[0]->getHeaders());
Check Server Logs:
Server::getLog(); // Returns Node.js server output
Validate PSR-7 Responses:
Ensure responses are PSR-7 compliant (use GuzzleHttp\Psr7\Response or convert Laravel responses):
use GuzzleHttp\Psr7\Utils;
$response = Utils::responseFromPsrResponse(
new \Symfony\Component\HttpFoundation\Response('OK', 200)
);
Test Timeouts: Add timeouts to client requests to avoid hanging:
$client = new Client(['timeout' => 2.0]);
Verify Raw Parts Byte-for-Byte:
When asserting on enqueueRawByteParts(), compare against the exact concatenation:
$expected = implode('', [
"data: first\n\n",
"data: second\n\n",
]);
expect((string) $response->getBody())->toBe($expected);
Default Ports: The server picks a random available port. To predict it:
$port = Server::start()['port'];
Host Binding:
By default, binds to localhost. Use 0.0.0.0 for external access:
Server::start(['host' => '0.0.0.0']);
Response Order: Responses are dequeued in FIFO order. Clear the queue between tests:
Server::clear();
Raw Parts Queueing Order:
enqueueRawByteParts() obeys the same FIFO queue as enqueue(). Mixing the two is supported, but keep each test's queue deterministic by calling Server::clear() in a beforeEach.
Custom Node.js Server: The package uses a pre-built Node.js server. For advanced use cases, you can:
PSR-7 Adapters: Convert Laravel/Symfony responses to PSR-7:
use GuzzleHttp\Psr7\Utils;
$psrResponse = Utils::responseFromPsrResponse(
new \Symfony\Component\HttpFoundation\Response()
);
Middleware Integration: Add middleware to modify requests/responses:
Server::enqueue(function ($request) {
$request = $request->withHeader('X-Test', 'true');
return new Response(200, [], 'Modified');
});
CI/CD Optimization:
FROM node:22
RUN npm install -g guzzle-test-server
How can I help you explore Laravel packages today?