Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Test Server Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. 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
    
  2. 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"}');
    });
    
  3. 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.

Implementation Patterns

Core Workflows

1. Mocking API Responses

// 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'),
]);

2. Validating Requests

$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]);

3. Testing Authentication

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);

4. Integration with Laravel

// 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');

5. Dynamic Ports (Advanced)

Server::start(['port' => 8080]); // Fixed port
Server::start(['host' => 'localhost', 'port' => 0]); // Random port

6. Streaming / Delayed Responses (1.1.0+)

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:

  • You need the exact bytes on the wire (whitespace, framing, delimiters).
  • You want the client to observe multiple flushes in sequence.
  • You're testing an SSE parser or a streaming HTTP client (Guzzle stream option, Http::withOptions(['stream' => true])).

Advanced Patterns

Conditional Responses

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');
});

Testing Webhooks

Simulate webhook payloads with headers:

Server::enqueue(new Response(200, [
    'X-Signature' => 'mock-sig',
    'Content-Type' => 'application/json',
], json_encode(['event' => 'payment.succeeded'])));

Error Scenarios

Mock 5xx errors or rate limits:

Server::enqueue(new Response(503, [], 'Service Unavailable'));

Testing SSE / Chunked Streams (1.1.0+)

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

Reusing the Server

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();
    }
}

Gotchas and Tips

Pitfalls

  1. Node.js Dependency:

    • Issue: Tests may fail silently if Node.js is missing.
    • Fix: Add a pre-test check:
      # In CI/CD or setup script
      node --version || exit 1
      
  2. Port Conflicts:

    • Issue: Random ports may clash with other services.
    • Fix: Use fixed ports or validate ports:
      Server::start(['port' => 8080]);
      
  3. Request Reconstruction:

    • Issue: Complex headers (e.g., Host) may not reconstruct correctly.
    • Fix: Update to >=0.4.0 (parses ports from Host headers).
  4. Windows Compatibility:

    • Issue: Older versions had startup issues.
    • Fix: Use >=0.3.1 or ensure Node.js is installed via nvm.
  5. Shutdown Race Conditions:

    • Issue: Server may not stop cleanly if tests fail.
    • Fix: Use register_shutdown_function or explicit try/catch:
      try {
          Server::start();
          // Tests...
      } finally {
          Server::stop();
      }
      
  6. Raw Byte Parts Are Literal:

    • Issue: 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.
    • Fix: Build parts from explicit strings, and test the parser against each part individually before assuming the concatenated stream is valid.
  7. No Automatic Headers on Raw Parts:

    • Issue: Because the parts are raw bytes, do not rely on 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.
    • Fix: Combine with a queued Response for the header-bearing request when needed, and keep enqueueRawByteParts() for the body-only streaming case.
  8. Streaming Client Timeouts:

    • Issue: A stream that never terminates will hang a test that expects a full body read.
    • Fix: Set a client-side timeout (e.g., ['timeout' => 2.0]) and/or read a fixed number of events before closing the stream.

Debugging Tips

  1. Inspect Received Requests:

    dd(Server::received()[0]->getHeaders());
    
  2. Check Server Logs:

    Server::getLog(); // Returns Node.js server output
    
  3. 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)
    );
    
  4. Test Timeouts: Add timeouts to client requests to avoid hanging:

    $client = new Client(['timeout' => 2.0]);
    
  5. 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);
    

Configuration Quirks

  1. Default Ports: The server picks a random available port. To predict it:

    $port = Server::start()['port'];
    
  2. Host Binding: By default, binds to localhost. Use 0.0.0.0 for external access:

    Server::start(['host' => '0.0.0.0']);
    
  3. Response Order: Responses are dequeued in FIFO order. Clear the queue between tests:

    Server::clear();
    
  4. 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.


Extension Points

  1. Custom Node.js Server: The package uses a pre-built Node.js server. For advanced use cases, you can:

    • Fork the Node.js server and extend it.
    • Example: Add support for WebSockets or custom protocols.
  2. PSR-7 Adapters: Convert Laravel/Symfony responses to PSR-7:

    use GuzzleHttp\Psr7\Utils;
    $psrResponse = Utils::responseFromPsrResponse(
        new \Symfony\Component\HttpFoundation\Response()
    );
    
  3. Middleware Integration: Add middleware to modify requests/responses:

    Server::enqueue(function ($request) {
        $request = $request->withHeader('X-Test', 'true');
        return new Response(200, [], 'Modified');
    });
    
  4. CI/CD Optimization:

    • Cache Node.js modules to speed up test suites.
    • Use Docker to isolate Node.js environments:
      FROM node:22
      RUN npm install -g guzzle-test-server
      
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
flareia/php-sdk
mrclay/userland-session
leafs/anchor
leafs/alchemy
leafs/password
leafs/session
leafs/date
leafs/http
leafs/form
actengage/metrics
roundly-consulting/webhooks-for-laravel
leandrodiogenes02/scribe
ahmednour/laravel-custom-reports
brainstud/file-vault
codingagency/laravel-spam-strike
skyraptor/sweet-alert
ipregistry/ipregistry-php
goodport/laravel-js-error-logger
devkussema/acapapay-laravel
smronju/nativephp-geolocation