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

Openapi Httpfoundation Testing Laravel Package

osteel/openapi-httpfoundation-testing

Validate Laravel HTTP test responses against an OpenAPI schema. Integrates with HTTP Foundation testing to assert that API responses match your OpenAPI specification, helping catch contract drift early.

Deep Wiki
Context7

Getting Started

Install (dev-only, always):

composer require --dev osteel/openapi-httpfoundation-testing

It pulls in league/openapi-psr7-validator (the actual validation engine) and symfony/psr-http-message-bridge, which converts HttpFoundation Request/Response objects into PSR-7 so the league validator can inspect them. That conversion is the whole value of the package — you never touch PSR-7 yourself.

Where to look first:

  1. The README's usage block — the public surface is tiny: ValidatorBuilder (loads the spec) and Validator (does the work).
  2. ValidatorBuilder::fromYamlFile() / fromJsonFile() / fromYamlString() / fromJsonString() and ->getValidator().
  3. Validator::validateRequest() and Validator::validateResponse() — the two methods you'll call 95% of the time.
  4. Exceptions from League\OpenAPIValidation\PSR7\Exception — they carry the real detail (getVerboseMessage() gives you the field-level reason).

First use case — pin one critical endpoint in a feature test:

use Osteel\OpenApi\Testing\ValidatorBuilder;

$validator = ValidatorBuilder::fromYamlFile(base_path('openapi/api.yaml'))->getValidator();

$response = $this->getJson('/api/users/1');

$validator->validateResponse($response->baseResponse, '/api/users/1', 'get', 200);

In Laravel, $this->getJson() returns a TestResponse; the HttpFoundation object you need is $response->baseResponse. In Symfony, it's $client->getResponse() directly.

Start with response validation on read endpoints only. It's the highest signal, lowest friction, and it immediately tells you whether your spec is actually in sync with reality (it usually isn't at first).

Implementation Patterns

Centralise the validator in a base test class. Building the validator parses the whole spec and resolves $refs — do it once per suite, not per test.

abstract class OpenApiTestCase extends TestCase
{
    protected static Validator $validator;

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();

        self::$validator = ValidatorBuilder::fromYamlFile(base_path('openapi/api.yaml'))
            ->getValidator();
    }
}

Guard it behind a static ?Validator $instance cache if you use several specs; setUpBeforeClass isn't available in some test harnesses (Pest, PHPUnit @dataProvider static contexts).

Wrap it in a fluent assertion helper. Raw validateResponse() calls drown the actual test intent. A small trait pays for itself immediately:

protected function assertValidResponse(TestResponse $response, string $path, string $method, int $status): void
{
    try {
        self::$validator->validateResponse($response->baseResponse, $path, $method, $status);
    } catch (ValidationFailed $e) {
        $this->fail(sprintf(
            "%s %s [%d] violates the OpenAPI spec:\n%s",
            strtoupper($method), $path, $status, $e->getVerboseMessage()
        ));
    }
}

Catching and re-throwing as a PHPUnit failure is important — otherwise you get a stack trace inside the league validator and lose the test name from the output.

Data-provider the happy paths. One test per endpoint, driven by a provider of [method, path, status], is the cheapest way to get whole-API coverage:

#[DataProvider('endpoints')]
public function test_endpoint_matches_spec(string $method, string $uri, int $status): void
{
    $response = $this->json($method, $uri);
    $this->assertValidResponse($response, $uri, strtolower($method), $status);
}

Keep the provider list short and curated — validating every route turns every spec edit into fifty red tests.

Validate the request as well when you have required fields. validateRequest() catches the classic bug where the client sends a field the spec says doesn't exist (additionalProperties: false) or omits a required one. Since Laravel builds a fresh Request during dispatch, validate the test's outgoing request before you send it, or capture $this->app['request'] after dispatch for the inbound one.

Contract-first workflow. Treat the spec as the source of truth in a dedicated tests/Contract/ directory, run it as its own CI job, and keep it out of the unit suite. A contract suite that fails should mean "the API changed or the spec drifted", not "a random model test broke".

Multiple specs, one per bounded context. fromYamlFile() is cheap enough to build a handful per test class; a public API spec and an internal admin spec rarely belong in the same file.

Use it as a spec-debugging tool, not just a test. When you're authoring a new OpenAPI file, a throwaway test that hits one endpoint and dumps $e->getVerboseMessage() is far faster than reading $ref resolution errors from a linter.

Gotchas and Tips

Paths must be concrete. validateResponse($r, '/users/1', 'get', 200) — not /users/{id}. The validator matches the literal URI against the spec's templated paths. Passing the template back is the single most common mistake.

Response validation needs the status code, and the status code must actually be declared in the spec for that operation. A 201 on an operation that only documents 200 fails with a path/response lookup error that reads like a routing problem but isn't.

servers: and basePath affect matching. If your spec declares servers: [{url: https://api.example.com/v1}], the validator may expect the /v1 prefix in the path you pass. Either strip servers from the spec used in tests or pass the full path. Symfony's basePath in the local router has the same effect.

Relative $refs resolve from the spec file's directory. fromYamlString() breaks external $refs because there's no base path — use the file-based constructors, or inline everything.

Content-Type is matched literally. If the spec only documents application/json and the framework returns application/json; charset=utf-8, most versions tolerate the parameter, but anything non-JSON (text/plain, application/problem+json) needs an explicit entry in content:.

Empty bodies. 204/304 responses with no content need the operation to declare no response body. Laravel's response()->noContent() produces a Response with an empty string body, which is fine, but a 200 documented with a schema and returned empty will fail — correctly.

additionalProperties: false is your best friend and your loudest alarm. It catches serializers leaking internal columns into API responses. Expect a rush of failures on the first run; that's the point.

Streamed and binary responses are not really supported. StreamedResponse and file downloads don't round-trip through the PSR-7 bridge meaningfully. Skip response validation for those; validate only status and headers if it matters.

Error messages are nested. ValidationFailed wraps league's ValidationException; always use getVerboseMessage() in your failure output. Without it you get "Request/Response validation failed" and nothing else.

oneOf/anyOf schemas produce noisy, hard-to-read errors because the validator reports the failure of each branch. If a schema is genuinely ambiguous, tighten it with a discriminator rather than fighting the error output.

Format keywords (date-time, email, uuid) are only enforced if your league validator version enables format checking — behaviour has shifted across versions. If a test passes that shouldn't, check composer show league/openapi-psr7-validator before you go hunting through your spec.

OpenAPI 3.1 support depends on the league validator version underneath, not on this package. Pin the transitive dependency if you're authoring 3.1 specs with type: [string, "null"] and friends.

Never wire this into production code paths. It's a test utility with a spec parser and a heavy dependency tree — keep it in require-dev and out of any request lifecycle.

Escape hatch for false positives: validate the response, then assert on specific payload fields separately. The schema check proves shape; it does not prove correctness. Don't let a green contract test stop you from writing the actual behavioural assertion.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo