studio-design / openapi-contract-testing
Framework-agnostic OpenAPI 3.0/3.1 contract testing for PHPUnit with endpoint coverage tracking
Package info
github.com/studio-design/openapi-contract-testing
pkg:composer/studio-design/openapi-contract-testing
Requires
- php: ^8.2
- opis/json-schema: ^2.6
- phpunit/phpunit: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- illuminate/testing: ^11.0 || ^12.0
- phpstan/phpstan: ^2.0
- symfony/http-foundation: ^6.4 || ^7.0 || ^8.0
Suggests
- illuminate/testing: Required for the Laravel adapter (ValidatesOpenApiSchema trait)
- dev-main
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-refactor/validator-cache-optimization
- dev-refactor/spec-loader-cache-optimization
- dev-refactor/remove-json-roundtrip
- dev-refactor/schema-converter-in-place
- dev-refactor/remove-phpstan-ignore-errors
- dev-feat/openapi-spec-attribute
- dev-renovate/configure
- dev-feat/ci-markdown-coverage-output
- dev-feat/support-php84-phpunit13
- dev-fix/ci-matrix-php82-phpunit12
This package is auto-updated.
Last update: 2026-03-17 10:01:45 UTC
README
Framework-agnostic OpenAPI 3.0/3.1 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.
Features
- OpenAPI 3.0 & 3.1 support — Automatic version detection from the
openapifield - Response validation — Validates response bodies against JSON Schema (Draft 07 via opis/json-schema). Supports
application/jsonand any+jsoncontent type (e.g.,application/problem+json) - Content negotiation — Accepts the actual response
Content-Typeto handle mixed-content specs. Non-JSON responses (e.g.,text/html,application/xml) are verified for spec presence without body validation; JSON-compatible responses are fully schema-validated - Endpoint coverage tracking — Unique PHPUnit extension that reports which spec endpoints are covered by tests
- Path matching — Handles parameterized paths (
/pets/{petId}) with configurable prefix stripping - Laravel adapter — Optional trait for seamless integration with Laravel's
TestResponse - Zero runtime overhead — Only used in test suites
Requirements
- PHP 8.2+
- PHPUnit 11, 12, or 13
- Redocly CLI (recommended for
$refresolution / bundling)
Installation
composer require --dev studio-design/openapi-contract-testing
Setup
1. Bundle your OpenAPI spec
This package expects a bundled (all $refs resolved) JSON spec file. Use Redocly CLI to bundle:
npx @redocly/cli bundle openapi/root.yaml --dereferenced -o openapi/bundled/front.json
Important: The
--dereferencedflag is required. Without it,$refpointers (e.g.,#/components/schemas/...) are preserved in the output, causingUnresolvedReferenceExceptionat validation time. The underlying JSON Schema validator (opis/json-schema) does not resolve OpenAPI$refreferences.
2. Configure PHPUnit extension
Add the coverage extension to your phpunit.xml:
<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>
| Parameter | Required | Default | Description |
|---|---|---|---|
spec_base_path |
Yes* | — | Path to bundled spec directory (relative paths resolve from getcwd()) |
strip_prefixes |
No | [] |
Comma-separated prefixes to strip from request paths (e.g., /api) |
specs |
No | front |
Comma-separated spec names for coverage tracking |
output_file |
No | — | File path to write Markdown coverage report (relative paths resolve from getcwd()) |
console_output |
No | default |
Console output mode: default, all, or uncovered_only (overridden by OPENAPI_CONSOLE_OUTPUT env var) |
*Not required if you call OpenApiSpecLoader::configure() manually.
3. Use in tests
With Laravel (recommended)
Publish the config file:
php artisan vendor:publish --tag=openapi-contract-testing
This creates config/openapi-contract-testing.php:
return [ 'default_spec' => '', // e.g., 'front' // Maximum number of validation errors to report per response. // 0 = unlimited (reports all errors). 'max_errors' => 20, ];
Set default_spec to your spec name, then use the trait — no per-class override needed:
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); } }
To use a different spec for a specific test class, add the #[OpenApiSpec] attribute:
use Studio\OpenApiContractTesting\OpenApiSpec; use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema; #[OpenApiSpec('admin')] class AdminGetUsersTest extends TestCase { use ValidatesOpenApiSchema; // All tests in this class use the 'admin' spec }
You can also specify the spec per test method. Method-level attributes take priority over class-level:
#[OpenApiSpec('front')] class MixedApiTest extends TestCase { use ValidatesOpenApiSchema; public function test_front_endpoint(): void { // Uses 'front' from class-level attribute } #[OpenApiSpec('admin')] public function test_admin_endpoint(): void { // Uses 'admin' from method-level attribute (overrides class) } }
Resolution priority (highest to lowest):
- Method-level
#[OpenApiSpec]attribute - Class-level
#[OpenApiSpec]attribute openApiSpec()method overrideconfig('openapi-contract-testing.default_spec')
Note: You can still override
openApiSpec()as before — it remains fully backward-compatible.
Framework-agnostic
You can use the #[OpenApiSpec] attribute with the OpenApiSpecResolver trait in any PHPUnit test:
use Studio\OpenApiContractTesting\OpenApiSpec; use Studio\OpenApiContractTesting\OpenApiSpecResolver; use Studio\OpenApiContractTesting\OpenApiResponseValidator; #[OpenApiSpec('front')] class GetPetsTest extends TestCase { use OpenApiSpecResolver; public function test_list_pets(): void { $specName = $this->resolveOpenApiSpec(); // 'front' $validator = new OpenApiResponseValidator(); $result = $validator->validate( specName: $specName, method: 'GET', requestPath: '/api/v1/pets', statusCode: 200, responseBody: $decodedJsonBody, responseContentType: 'application/json', ); $this->assertTrue($result->isValid(), $result->errorMessage()); } }
Or without the attribute, pass the spec name directly:
use Studio\OpenApiContractTesting\OpenApiResponseValidator; use Studio\OpenApiContractTesting\OpenApiSpecLoader; // Configure once (e.g., in bootstrap) OpenApiSpecLoader::configure(__DIR__ . '/openapi/bundled', ['/api']); // In your test $validator = new OpenApiResponseValidator(); $result = $validator->validate( specName: 'front', method: 'GET', requestPath: '/api/v1/pets', statusCode: 200, responseBody: $decodedJsonBody, responseContentType: 'application/json', // optional: enables content negotiation ); $this->assertTrue($result->isValid(), $result->errorMessage());
Controlling the number of validation errors
By default, up to 20 validation errors are reported per response. You can change this via the constructor:
// Report up to 5 errors $validator = new OpenApiResponseValidator(maxErrors: 5); // Report all errors (unlimited) $validator = new OpenApiResponseValidator(maxErrors: 0); // Stop at first error (pre-v0.x default) $validator = new OpenApiResponseValidator(maxErrors: 1);
For Laravel, set the max_errors key in config/openapi-contract-testing.php.
Coverage Report
After running tests, the PHPUnit extension prints a coverage report. The output format is controlled by the console_output parameter (or OPENAPI_CONSOLE_OUTPUT environment variable).
default mode (default)
Shows covered endpoints individually and uncovered as a count:
OpenAPI Contract Test Coverage
==================================================
[front] 12/45 endpoints (26.7%)
--------------------------------------------------
Covered:
✓ GET /v1/pets
✓ POST /v1/pets
✓ GET /v1/pets/{petId}
✓ DELETE /v1/pets/{petId}
Uncovered: 41 endpoints
all mode
Shows both covered and uncovered endpoints individually:
OpenAPI Contract Test Coverage
==================================================
[front] 12/45 endpoints (26.7%)
--------------------------------------------------
Covered:
✓ GET /v1/pets
✓ POST /v1/pets
✓ GET /v1/pets/{petId}
✓ DELETE /v1/pets/{petId}
Uncovered:
✗ PUT /v1/pets/{petId}
✗ GET /v1/owners
...
uncovered_only mode
Shows uncovered endpoints individually and covered as a count — useful for large APIs where you want to focus on missing coverage:
OpenAPI Contract Test Coverage
==================================================
[front] 12/45 endpoints (26.7%)
--------------------------------------------------
Covered: 12 endpoints
Uncovered:
✗ PUT /v1/pets/{petId}
✗ GET /v1/owners
...
You can set the mode via phpunit.xml:
<parameter name="console_output" value="uncovered_only"/>
Or via environment variable (takes priority over phpunit.xml):
OPENAPI_CONSOLE_OUTPUT=uncovered_only vendor/bin/phpunit
CI Integration
GitHub Actions Step Summary
When running in GitHub Actions, the extension automatically detects the GITHUB_STEP_SUMMARY environment variable and appends a Markdown coverage report to the job summary. No configuration needed.
Note: Both features are independent — when running in GitHub Actions with
output_fileconfigured, the Markdown report is written to both the file and the Step Summary.
Markdown output file
Use the output_file parameter to write a Markdown report to a file. This is useful for posting coverage as a PR comment:
<extensions> <bootstrap class="Studio\OpenApiContractTesting\PHPUnit\OpenApiCoverageExtension"> <parameter name="spec_base_path" value="openapi/bundled"/> <parameter name="specs" value="front,admin"/> <parameter name="output_file" value="coverage-report.md"/> </bootstrap> </extensions>
You can also use the OPENAPI_CONSOLE_OUTPUT environment variable in CI to show uncovered endpoints in the job log:
- name: Run tests (show uncovered endpoints) run: vendor/bin/phpunit env: OPENAPI_CONSOLE_OUTPUT: uncovered_only
Example GitHub Actions workflow step to post the report as a PR comment:
- name: Run tests run: vendor/bin/phpunit - name: Post coverage comment if: github.event_name == 'pull_request' && hashFiles('coverage-report.md') != '' uses: marocchino/sticky-pull-request-comment@v2 with: path: coverage-report.md
OpenAPI 3.0 vs 3.1
The package auto-detects the OAS version from the openapi field and handles schema conversion accordingly:
| Feature | 3.0 handling | 3.1 handling |
|---|---|---|
nullable: true |
Converted to type array ["string", "null"] |
Not applicable (uses type arrays natively) |
prefixItems |
N/A | Converted to items array (Draft 07 tuple) |
$dynamicRef / $dynamicAnchor |
N/A | Removed (not in Draft 07) |
examples (array) |
N/A | Removed (OAS extension) |
readOnly / writeOnly |
Removed (OAS-only in 3.0) | Preserved (valid in Draft 07) |
API Reference
OpenApiResponseValidator
Main validator class. Validates a response body against the spec.
The constructor accepts a maxErrors parameter (default: 20) that limits how many validation errors the underlying JSON Schema validator collects. Use 0 for unlimited, 1 to stop at the first error.
The optional responseContentType parameter enables content negotiation: when provided, non-JSON content types (e.g., text/html) are checked for spec presence only, while JSON-compatible types proceed to full schema validation.
$validator = new OpenApiResponseValidator(maxErrors: 20); $result = $validator->validate( specName: 'front', method: 'GET', requestPath: '/api/v1/pets/123', statusCode: 200, responseBody: ['id' => 123, 'name' => 'Fido'], responseContentType: 'application/json', ); $result->isValid(); // bool $result->errors(); // string[] $result->errorMessage(); // string (joined errors) $result->matchedPath(); // ?string (e.g., '/v1/pets/{petId}')
OpenApiSpecLoader
Manages spec loading and configuration.
OpenApiSpecLoader::configure('/path/to/bundled/specs', ['/api']); $spec = OpenApiSpecLoader::load('front'); OpenApiSpecLoader::reset(); // For testing
OpenApiCoverageTracker
Tracks which endpoints have been validated.
OpenApiCoverageTracker::record('front', 'GET', '/v1/pets'); $coverage = OpenApiCoverageTracker::computeCoverage('front'); // ['covered' => [...], 'uncovered' => [...], 'total' => 45, 'coveredCount' => 12]
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.