blastcloud/guzzler
Guzzler is a lightweight, Laravel-friendly testing helper for Guzzle that lets you fake HTTP requests and assert on what was sent. Define expected requests, return canned responses, and verify methods, URLs, headers, and bodies in your PHP tests without hitting real APIs.
Install as a dev dependency; it is a test-only package and has no business being in production composer.json:
composer require --dev blastcloud/guzzler
The entry point is the UsesGuzzler trait. Add it to a test class and everything you need hangs off $this->guzzler and $this->getClient():
use BlastCloud\Guzzler\UsesGuzzler;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
class InvoiceSyncTest extends TestCase
{
use UsesGuzzler;
public static function setUpBeforeClass(): void
{
static::setUpGuzzler(); // initialises mock history
}
public function test_syncs_invoices(): void
{
$this->guzzler->expects($this->once())
->post('/v1/invoices')
->withJson(['customer' => 'cus_123'])
->willRespond(new Response(201, [], json_encode(['id' => 'inv_1'])));
$client = $this->getClient(); // Guzzle client wired to Guzzler
$service = new InvoiceService($client);
$service->sync('cus_123');
$this->guzzler->assertAll(); // every expectation was hit
}
}
Two things to internalise first: the client must come from $this->getClient(), and you should call $this->guzzler->assertAll() at the end of the test (or in tearDown) to fail when an expectation was never matched. That pair — a mock-backed client and a global assertion — is the whole mental model.
The first use case is almost always: an application service calls a third-party HTTP API (Stripe, OpenWeather, an internal microservice), and you want a fast, deterministic test that asserts the outbound contract without touching the network.
Inject the Guzzler client, don't new Client(). In Laravel, bind it before exercising the code under test:
$this->app->instance(\GuzzleHttp\ClientInterface::class, $this->getClient());
// or bind the concrete Client, or a wrapper service that accepts a client
If your code resolves a wrapper/repository around Guzzle, bind the mock client into that wrapper — it's cleaner than reaching into the container for Client.
Arrange multiple expectations to sequence responses. Each expects() call is matched in order, so repeated calls to the same endpoint return successive responses:
$this->guzzler->expects($this->once())
->get('/stock/AAPL')
->willRespond(new Response(200, [], json_encode(['px' => 100])));
$this->guzzler->expects($this->once())
->get('/stock/AAPL')
->willRespond(new Response(500, [], 'upstream down'));
Match on headers, query, and body to pin the outbound contract; these are the assertions you actually want to fail loudly when a payload changes:
$this->guzzler->expects($this->once())
->post('/v1/charges')
->withHeader('Authorization', 'Bearer sk_test_x')
->withJson(['amount' => 5000, 'currency' => 'usd'])
->willRespond(new Response(200, [], json_encode(['id' => 'ch_1'])));
Callback responses carry dynamic logic — useful when the returned payload depends on the request (echoing an ID, varying by query string) or when you want to assert on the request at response time:
->willRespond(function (RequestInterface $request) {
$id = json_decode((string) $request->getBody(), true)['customer'];
return new Response(200, [], json_encode(['customer' => $id]));
});
Exceptions use willThrow, which is how you test retry/circuit-breaker logic without flaky networking:
->willThrow(new \GuzzleHttp\Exception\ConnectException(...));
Wildcards and regex for URL matching keep expectations resilient to path parameters, while still requiring the method to match:
->get('/users/*/orders') // any single path segment
->url('#^/users/\d+/orders$#') // explicit regex
Inspect the call log for the "right thing was sent" assertions — $this->guzzler->getRequests() returns the captured RequestInterface collection, so you can assert on bodies Guzzler didn't match against.
Prefer Guzzler over Laravel's Http::fake() when the code under test talks to Guzzle directly; use Http::fake() when the code uses Laravel's Http facade. Mixing both in one suite is fine but be explicit about which layer you're mocking.
new \GuzzleHttp\Client() inside a test bypasses Guzzler entirely and will attempt a real request. Every client used by the code under test must descend from $this->getClient() (or the container binding).assertAll() is not automatic. Without it, an expectation that was never invoked is silently ignored and your test passes green despite a dead API call. Put it in tearDown once rather than remembering it per-test, or call it explicitly at the end of each test.MockHandler exceptions from Guzzle. If order genuinely doesn't matter, register expectations in call order anyway — it makes the test read like the workflow.User-Agent, Host, Content-Length, etc. Match only the headers you care about rather than asserting the entire header set.withJson() (structural) or a wildcard. Guzzle re-encodes JSON, so asserting on a raw "{"a":1}" string is brittle; prefer withJson().new Response(200, [], ['a' => 1]) will not serialise the way you expect; json_encode() (or a PSR-7 stream) is required. Same trap applies to array headers versus string values.willRespond with a callable is the extension point for fixtures, generators, and assertions on the request. It is where you wire in Faker data or verify signed payloads.Guzzler on setup, $this->guzzler is safe to reuse across methods — but if you override setUp without calling the trait's bootstrap, history and expectations leak between tests.MockHandler exception listing recent requests; when you see it, read the log before hunting through application code — the problem is almost always a mismatched method/URL/expectation count, not the code under test.How can I help you explore Laravel packages today?