studio-design/openapi-contract-testing
Gesso
/ˈdʒɛs.so/ — pronounced “JESS-so”
Gesso is the primer applied to a canvas before painting—a stable, receptive ground on which the finished work can be built. Gesso brings that same idea to APIs, providing a dependable foundation for OpenAPI contract testing in PHP.
Gesso provides framework-agnostic OpenAPI 3.0/3.1/3.2 contract testing for PHPUnit with endpoint coverage tracking.
Validate your API responses against your OpenAPI specification during testing, and get a coverage report showing which endpoints have been tested.
Gesso remains distributed as studio-design/openapi-contract-testing; existing Composer requirements, PHP namespaces, and configuration keys do not change.
Search the documentation · Core quickstart · Laravel · Symfony · Pest
Features
- OpenAPI 3.0, 3.1 & 3.2 support — Explicit version detection, including 3.2
QUERY, customadditionalOperations, formquerystring,discriminator.defaultMapping, and observable streaming limitations - Response & request validation — dialect-aware JSON Schema via opis/json-schema: Draft 07 compatibility for OpenAPI 3.0 and native 2020-12 semantics for OpenAPI 3.1/3.2;
application/jsonand any+jsoncontent type - Endpoint coverage tracking — Unique PHPUnit extension that reports which spec endpoints are covered by tests, at
(method, path, status, content-type)granularity - Laravel route/spec parity —
openapi:routesfinds documented operations without routes and registered routes without OpenAPI operations, with filters, stable JSON, and independent CI gates - Schema-driven request fuzzing — Valid boundaries, composition branches, targeted negative cases with explicit expected status classes, deterministic replay/reduction, whole-spec filters, lifecycle/auth hooks, and explicit skip reasons
- Enum drift detection — Static comparison between PHP backed enums and their
enum:spec arrays, with PHPUnit-extension auto-discovery - Schema under-description detection — Optional strict mode that flags response fields the implementation always returns but the spec marks as optional, catching the spec gaps that conformance checks alone can't. See
docs/strict-required.mdfor current scope and limitations. - Skip-by-status-code — Configurable regex list of status codes whose bodies are not validated (default: every
5xx); per-request viaskipResponseCode() - PSR-7, Laravel, Symfony & Pest adapters — First-class PSR-7 request/response/exchange validation, auto-assert / auto-validate-request integration for Laravel, HttpFoundation assertions for Symfony, and Pest expectations
- Parallel-runner safe — Coordinated sidecar+merge workflow for paratest /
pest --parallel - Multi-format reports — Markdown / JUnit XML / JSON / HTML output with one-click GitHub Step Summary
- Zero runtime overhead — Only used in test suites
Why this library?
Choose based on the workflow you need rather than on a single yes/no feature count:
- Choose this library when you need response-level coverage at
(method, path, status, content-type)granularity, several CI report formats, OpenAPI 3.1/3.2 JSON Schema semantics, schema-driven exploration, or drift detection across a framework-agnostic core and Laravel, Symfony, and Pest adapters. - Choose Spectator for a Laravel 12 application when generated test stubs, JSON assertion failures, or remote/private-GitHub spec sources matter more than response-level coverage granularity and broader framework support.
- Choose league/openapi-psr7-validator when you want a low-level PSR-7 validator or PSR-15 middleware and will build the test/reporting integration yourself.
- Choose osteel/openapi-httpfoundation-testing when you want a small HttpFoundation-to-PSR-7 validation bridge, or laravel-openapi-validator when automatic validation around Laravel HTTP tests is the main requirement.
Feature comparison (checked 2026-07-10)
Legend: ✅ supported · — no equivalent feature documented. “Not documented” is intentionally different from “unsupported”.
Methodology: This is a documentation/source audit, not a benchmark. Claims are limited to the linked, tag-pinned public documentation and Composer constraints checked on 2026-07-10. This-library claims describe main at 8c6416d; competitor versions are shown in the table header. Re-check this matrix using the release checklist at least quarterly or before a release when three months have elapsed.
Requirements
- PHP 8.2+
- PHPUnit 11, 12, or 13
- A PSR-18 HTTP client + PSR-17 request factory (e.g. Guzzle, Symfony HttpClient) — only required when resolving HTTP(S)
$refs
Installation
composer require --dev studio-design/openapi-contract-testing
YAML specs require
symfony/yaml. It is listed undersuggestso it isn't installed automatically. If your spec is JSON, you can skip this. If your spec is.yaml/.yml, add it explicitly:composer require --dev symfony/yamlWithout it, the loader throws
InvalidOpenApiSpecExceptionwith a clear "requires symfony/yaml" message the first time it tries to read a YAML file.
Quick start
Choose the CI-tested five-minute path matching your stack:
| Stack | Passing example | What it demonstrates |
|---|---|---|
| Framework-independent PHPUnit | examples/core |
Direct response validation and coverage |
| Laravel | examples/laravel |
Explicit assertion, auto_assert, and request validation |
| Symfony | examples/symfony |
HttpFoundation request/response assertions |
| Pest | examples/pest |
Laravel response and request expectations |
| PSR-7 | examples/psr7 |
Request/response exchange validation |
All paths start with the same development dependency:
composer require --dev studio-design/openapi-contract-testing
The example below uses a PSR-7 request and response. The searchable documentation contains the complete core, Laravel, Symfony, and Pest quickstarts.
1. Provide your OpenAPI spec
Point the loader at your spec's entry file. Internal and local-filesystem $ref are resolved automatically — no pre-bundling required:
openapi/
├── root.yaml # paths reference ./schemas/*.yaml
└── schemas/
├── pet.yaml
└── error.json
2. Register the PHPUnit extension
Before running your first test, verify that the package can load and enforce the contract:
vendor/bin/openapi-contract doctor \
--spec=openapi/root.yaml \
--strip-prefix=/api \
--phpunit-snippet
The command resolves local references, checks the OpenAPI/JSON Schema dialect, reports unsupported enforcement features, counts discovered operations and responses, and exits non-zero for incompatible specs. Use --format=json in CI. See the doctor command reference for multiple specs, HTTP references, output categories, and exit codes.
Then register the emitted configuration:
<extensions>
<bootstrap class="Studio\OpenApiContractTesting\PHPUnit\OpenApiCoverageExtension">
<parameter name="spec_base_path" value="openapi/bundled"/>
<parameter name="strip_prefixes" value="/api"/>
<parameter name="specs" value="front,admin"/>
</bootstrap>
</extensions>
3. Validate a PSR-7 exchange
When your application or HTTP client already returns PSR-7 messages, validate both sides and record coverage with one framework-independent call:
use Studio\OpenApiContractTesting\Psr7\OpenApiPsr7Validator;
$validator = new OpenApiPsr7Validator('front');
$result = $validator->validateExchange($request, $response);
$this->assertTrue($result->isValid(), $result->errorMessage());
The adapter accepts any psr/http-message implementation; no concrete PSR-7
package is added to production dependencies. A PHPUnit assertion trait,
response-only operation addressing, PSR-15 test recipe, and stream guarantees
are covered in the PSR-7 guide.
Laravel adapter
php artisan vendor:publish --tag=openapi-contract-testing
Set default_spec in the published config/openapi-contract-testing.php, then mix in the trait:
use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema;
class GetPetsTest extends TestCase
{
use ValidatesOpenApiSchema;
public function test_list_pets(): void
{
$response = $this->get('/api/v1/pets');
$response->assertOk();
$this->assertResponseMatchesOpenApiSchema($response);
}
}
Before running tests, compare Laravel's registered routes with the spec:
php artisan openapi:routes --fail-on-undocumented --fail-on-unimplemented
To validate every response automatically, set 'auto_assert' => true and drop the explicit assert call. To also catch request-side drift, set 'auto_validate_request' => true. See docs/setup.md for the full configuration and opt-out reference.
Documentation
| Topic | Reference |
|---|---|
| PSR-7 request / response / exchange validation and PSR-15 test recipe | docs/psr7.md |
Full setup, Laravel / Symfony / framework-agnostic adapters, auto-assert, opt-out attributes, request validation, HTTP $ref |
docs/setup.md |
Pre-test compatibility diagnostics (openapi-contract doctor) |
docs/doctor.md |
Laravel route/spec parity (openapi:routes) |
docs/laravel-route-parity.md |
Pest plugin: expect()->toMatchOpenApiResponseSchema() and friends |
docs/pest-plugin.md |
| Schema-driven request fuzzing | docs/fuzzing.md |
| Enum drift detection | docs/enum-drift.md |
Schema under-description detection (strict_required) |
docs/strict-required.md |
| Coverage report modes & threshold gate | docs/coverage.md |
| HTML coverage output | docs/coverage-html-output.md |
| JSON coverage output schema | docs/coverage-json-schema.md |
Parallel test runners (paratest / Pest --parallel) |
docs/parallel.md |
| CI integration (GitHub Actions, PR comments, output formats, partial-run handling) | docs/ci.md |
API reference (OpenApiResponseValidator, OpenApiSpecLoader, OpenApiCoverageTracker) |
docs/api-reference.md |
| Supported features, known limitations, warning channel | docs/supported-features.md |
| Versioning policy & support matrix | docs/versioning.md |
Development
composer install
# Run tests
vendor/bin/phpunit
# Static analysis
vendor/bin/phpstan analyse
# Code style
vendor/bin/php-cs-fixer fix
vendor/bin/php-cs-fixer fix --dry-run --diff # Check only
License
MIT License. See LICENSE for details.