spurwork/spectator
Spectator is a lightweight Laravel package for observing outgoing HTTP requests, letting you inspect, log, and assert on the calls your app makes so you can debug integrations and write reliable tests without hitting external APIs.
Spectator is a contract-testing package: it validates your Laravel HTTP tests against an OpenAPI 3 document, so the spec becomes an executable part of the test suite rather than stale documentation.
composer require --dev hotmeteor/spectator
php artisan vendor:publish --tag=spectator-config
config/spectator.php, register your spec under sources and — critically — set path_prefix if your API lives behind a prefix such as /api:
'sources' => [
'local' => base_path('openapi.json'),
],
'path_prefix' => 'api',
setUp():
protected function setUp(): void
{
parent::setUp();
Spectator::using('openapi.json');
}
php artisan spectator:routes
As of v3.0.4 this command prints the resolved path including the configured prefix, which makes a misconfigured path_prefix immediately obvious instead of silently producing no matches.First use case: take one existing feature test hitting GET /api/users/{id} and add the two contract assertions. If they pass, you have a working baseline; if they fail, the error message names the exact JSON path that diverged — fix the spec or the endpoint, then repeat per resource.
Baseline happy path — the daily bread-and-butter assertion pair:
$response = $this->getJson('/api/users/1');
$response->assertValidRequest()
->assertValidResponse(200);
assertValidRequest() checks the outgoing test request against the spec (method, path params, query, headers, body); assertValidResponse() checks the actual response body/headers against the declared schema for that status code.
Contract-first workflow. Write or update the OpenAPI operation before the controller, then let the failing test define the shape. This is where Spectator earns its keep: the spec diff and the code diff land in the same PR and cannot drift apart unnoticed.
Layer contract and behaviour assertions. Spectator only proves shape; keep your normal Laravel assertions for semantics:
$response->assertValidResponse(200)
->assertJsonPath('data.0.email', '[email protected]');
$this->assertDatabaseHas('users', ['id' => 1]);
Negative-path testing. Pair the happy path with error-shape tests so your 4xx bodies are covered by the contract too:
$this->postJson('/api/users', [])->assertValidResponse(422);
Use the invalid-request/invalid-response assertions when you deliberately want to assert that something does not conform (e.g. proving a validation guard rejects malformed payloads).
Multiple specs. For a versioned or segmented API (openapi-v1.json, openapi-v2.json), flip the spec per test with Spectator::using(...) in the individual test or a trait, rather than a single global setUp.
Form and urlencoded endpoints. v3.0.4 makes an empty application/x-www-form-urlencoded body resolve to stdClass rather than []. Without that, an empty form POST validated against an object-typed request schema threw a 500 from the validator rather than a clean contract failure. If you test "submit empty form" cases (search filters, settings forms, unsubscribe flows), those tests are now viable — add them.
CI gate. Run the full suite on every PR; treat a spec/implementation mismatch as a failing build. Optionally add a composer test:contract script filtering on a Contract test suite so contract drift is reported separately from logic regressions.
path_prefix is the number one silent failure. If it's wrong, routes simply never match the spec and assertions pass vacuously or fail confusingly. Since v3.0.4, spectator:routes shows the resolved prefixed path — always eyeball that output after changing route groups or adding a versioned prefix like api/v2.postJson() against a spec operation declared only as multipart/form-data (or vice versa) fails validation for reasons that look like schema errors. Declare every content type you actually accept in the spec.additionalProperties: false, missing required entries, and untyped nullable fields are the usual culprits when a genuinely correct response fails. Generate the schema from your API Resource/casts rather than hand-writing it, and keep nullable consistent on both sides.example; the validator's error messages then point at a concrete offending value./data/0/id). Go straight to that node — it's almost always a type mismatch ("1" vs 1), a missing required key, or an unexpected extra key.require-dev only. Keep Spectator out of production autoload; if you use --no-dev deploys this is automatic, but check any custom CI images that install with dev dependencies.Spectator::using() in a base test case or trait when you need per-suite defaults.spectator:routes output. No breaking changes, no config changes required — just composer update hotmeteor/spectator. The one behavioural difference to be aware of: if you had a test asserting a 500 (or skipping) empty form POSTs against an object schema, it will now validate normally and should be rewritten as a real contract assertion.How can I help you explore Laravel packages today?