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

Swagger Assertions Laravel Package

fr3d/swagger-assertions

Laravel testing package adding PHPUnit assertions that validate API responses against your Swagger/OpenAPI definitions. Use it in feature tests to check status codes, schemas, and response structures, keeping documentation and endpoints in sync and catching contract drift early.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the package (dev only — it's a test dependency):

composer require --dev fr3d/swagger-assertions:^1.0

Pin to ^1.0. The v1.0.0 release is the first one in years and is explicitly a break from the pre-1.0 line (dependency removal + new supported PHP/PHPUnit range) — don't let Composer float you across the boundary in a repo that's already on 0.x.

2. Find your Swagger document. This library validates Swagger 2.0 (swagger: "2.0"), not OpenAPI 3.x. If you're generating docs with darkaonline/l5-swagger, check which spec you're emitting — if it's OAS3, this package will not parse it. You need a swagger.json/swagger.yaml on disk that describes your API paths.

3. Register the schema once, in a base test case. The schema is registered statically, so do it in setUpBeforeClass() of an abstract ApiTestCase that other tests extend:

namespace Tests;

use PHPUnit\Framework\TestCase;
use SwaggerAssertions\PhpUnit\AssertsTrait;
use SwaggerAssertions\SwaggerAssertions;

abstract class ApiContractTestCase extends TestCase
{
    use AssertsTrait;

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();
        SwaggerAssertions::registerSchemaFromFile(base_path('docs/swagger.json'));
    }
}

The entry point is registerSchemaFromFile() in the current line; the trait file is small, so skim it once on upgrade to confirm nothing renamed.

4. First use case — validate one endpoint: write a single test that hits a real endpoint and asserts the response matches the documented operation:

public function test_index_users_matches_contract(): void
{
    $laravel = $this->get('/api/users');
    $psr7 = $this->toPsr7($laravel);

    static::assertResponseMatches($psr7, '/users', 'get');
}

The second argument is the templated path from your swagger file (/users/{id}), not the concrete URL you requested. That's the single most common source of confusion — see Gotchas.

Implementation Patterns

Bridge TestResponse → PSR-7. The assertions accept Psr\Http\Message\ResponseInterface / ServerRequestInterface, which Laravel's test response is not. Add a small helper to your base test case:

use GuzzleHttp\Psr7\Response as Psr7Response;

protected function toPsr7(TestResponse $response): Psr7Response
{
    return new Psr7Response(
        $response->getStatusCode(),
        $response->headers->all(),           // array<string, string[]>
        $response->getContent()
    );
}

Build the PSR-7 object yourself from Laravel primitives. v1.0.0 dropped the laminas/laminas-http dependency, so don't assume any HTTP-message types come in transitively — declare guzzlehttp/psr7 explicitly in require-dev, and if your helper happened to construct Laminas\Http\Response objects, that path is gone.

If you need request assertions (query params, body schema on inbound requests), build a GuzzleHttp\Psr7\ServerRequest explicitly from the payload you sent rather than trying to reverse-engineer it out of the Laravel test response — it's clearer and keeps the test intent obvious.

Assert on both the happy path and the error contract. A big chunk of the value here is confirming your error responses are documented and shaped correctly:

$response = $this->toPsr7($this->getJson('/api/users/999999'));
static::assertResponseMatches($response, '/users/{id}', 'get');

This will fail loudly if 404 isn't in the responses block, or if your error envelope drifts from the documented Error schema.

Data-provider sweep for coverage. Rather than hand-writing a test per endpoint, drive it from the swagger document itself — but do it as a smoke layer, not a replacement for behavior tests:

/**
 * @dataProvider endpointProvider
 */
public function test_every_documented_operation_responds(string $path, string $method): void
{
    $response = $this->toPsr7($this->json($method, $this->sampleUrl($path)));
    static::assertResponseMatches($response, $path, $method);
}

Keep the provider list explicit ([['/users','get'], ['/users/{id}','get']]) rather than auto-parsing the schema — an explicit list documents which endpoints you actually cover and fails visibly when you add a new one.

Use it as a drift detector in CI, not as your primary test suite. Wire it into a phpunit --testsuite=contract group so you can run it on every PR without bloating the fast unit suite. Treat a failure here as "either the implementation or the swagger file changed without the other" — that's the actual signal.

Pair it with schema generation. The pattern that works long-term is: generate swagger.json from annotations (l5-swagger) → validate responses against the generated file. That closes the loop, because a stale schema is the only failure mode that hurts. Hand-maintained swagger files rot fast and turn this library into a nuisance.

Request-side assertions for write endpoints. For POST/PUT, assert the request body and query parameters against the documented parameters before sending, so you catch a test that's exercising an undocumented code path:

static::assertRequestMatches($psr7Request, '/users', 'post');

Gotchas and Tips

  • Swagger 2.0 only. No oneOf/anyOf/nullable (OAS3 semantics), no components. The v1.0.0 release modernised tooling, not the spec support. If your team has migrated to OpenAPI 3, this library is a dead end — look at league/openapi-psr7-validator instead, which handles both and is actively maintained.
  • v1.0.0 is a breaking bump — read your lockfile diff. Two changes matter for consumers: (a) support matrix moved to current PHP/PHPUnit/Symfony, so old pins may now conflict; (b) laminas/laminas-http was removed. If anything in your test helpers leaned on that package being installed transitively, Composer will now fail or your helper will fatal — add an explicit require-dev entry or refactor the helper. Expect to spend one commit on the upgrade, not zero.
  • Re-baseline your Content-Type matching after upgrading. Header handling used to be partly delegated to the Laminas HTTP layer; it's now in-package. Before you trust the suite again, run one endpoint whose spec produces is application/json while Laravel returns application/json; charset=utf-8. If your version no longer normalises the parameter, set the header explicitly in the response or drop the charset from the spec.
  • Path argument must be templated. assertResponseMatches($r, '/users/42', 'get') fails; you must pass /users/{id}. Build a small templatize() helper or keep a map of route → swagger path.
  • Every status code must be documented. The validator looks up responses.{status} in the operation. An undocumented 422 or 500 throws. This is the library doing its job, but it surprises people on the first run against a real API.
  • Schema registration is global static state. Registering in multiple test classes, or in setUp() with different files, leads to order-dependent failures. Register exactly once in a base class; use @runInSeparateProcess only as a last resort.
  • additionalProperties defaults are strict. A response body containing a field not in the schema fails. This catches real drift (good) but also catches debug payloads leaking into responses (also good) and Laravel's pagination meta/links wrappers if you haven't modeled them (annoying — model them once, in a shared definition).
  • Type coercion gotchas. Integer IDs serialized as strings ("id": "5") will fail against type: integer. Eloquent's $casts usually saves you, but check models with string primary keys.
  • Debugging: the thrown exception message points at a JSON pointer into the document (e.g. #/definitions/User/properties/email). Read it literally — it tells you whether the response or the schema is wrong, and that distinction is the whole diagnosis.
  • PHPUnit compatibility is now current — but verify on your matrix. v1.0.0 claims testing against the latest supported PHP, PHPUnit and Symfony. That retires the old "pin PHPUnit 9 or vendor a patch" advice, but it also means the supported floor moved up: if you're still on PHP 8.0 or PHPUnit 8, you can't take ^1.0 and should stay on the 0.x line (and accept the old friction). Run the contract suite once on your lowest and highest CI job before rolling it out repo-wide.
  • Maintenance signal is better, governance signal is not. v1.0.0 plus a dependency cleanup indicates someone is still tending it, which softens the "don't expect upstream fixes" warning. The license file still needs verifying in the repo before adopting in a commercial codebase — NOASSERTION on the metadata may not have changed with this release. It's still a small codebase, so forking remains the pragmatic plan if you commit to it.
  • Extension point: the trait methods delegate to a schema-backed validator object; if you need custom failure messages or want to soft-fail (log instead of throw) during a migration, wrap the static assertions in your own base-class methods rather than calling them directly from tests. That gives you one place to swap the implementation later — and one place to absorb the next major bump.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource