PHPackages                             studio-design/openapi-contract-testing - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. studio-design/openapi-contract-testing

ActiveLibrary[Testing &amp; Quality](/categories/testing)

studio-design/openapi-contract-testing
======================================

Framework-agnostic OpenAPI 3.0/3.1 contract testing for PHPUnit with endpoint coverage tracking

v0.11.0(2mo ago)0802↓23.6%[1 PRs](https://github.com/studio-design/openapi-contract-testing/pulls)MITPHPPHP ^8.2CI passing

Since Feb 19Pushed 2mo agoCompare

[ Source](https://github.com/studio-design/openapi-contract-testing)[ Packagist](https://packagist.org/packages/studio-design/openapi-contract-testing)[ RSS](/packages/studio-design-openapi-contract-testing/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (11)Versions (26)Used By (0)

OpenAPI Contract Testing for PHPUnit
====================================

[](#openapi-contract-testing-for-phpunit)

[![CI](https://github.com/studio-design/openapi-contract-testing/actions/workflows/ci.yml/badge.svg)](https://github.com/studio-design/openapi-contract-testing/actions/workflows/ci.yml)[![Latest Stable Version](https://camo.githubusercontent.com/2026a9bf264d6e431221bdb13b17f8bdd994a589a32254e99831587fd805c84f/68747470733a2f2f706f7365722e707567782e6f72672f73747564696f2d64657369676e2f6f70656e6170692d636f6e74726163742d74657374696e672f76)](https://packagist.org/packages/studio-design/openapi-contract-testing)[![License](https://camo.githubusercontent.com/45697427d7f8fb055b2789def706950a58ba48a86dc78f54b1472c9ae0539002/68747470733a2f2f706f7365722e707567782e6f72672f73747564696f2d64657369676e2f6f70656e6170692d636f6e74726163742d74657374696e672f6c6963656e7365)](https://packagist.org/packages/studio-design/openapi-contract-testing)

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
--------

[](#features)

- **OpenAPI 3.0 &amp; 3.1 support** — Automatic version detection from the `openapi` field
- **Response validation** — Validates response bodies against JSON Schema (Draft 07 via opis/json-schema). Supports `application/json` and any `+json` content type (e.g., `application/problem+json`)
- **Content negotiation** — Accepts the actual response `Content-Type` to 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
------------

[](#requirements)

- PHP 8.2+
- PHPUnit 11, 12, or 13
- [Redocly CLI](https://redocly.com/docs/cli/) (recommended for `$ref` resolution / bundling)

Installation
------------

[](#installation)

```
composer require --dev studio-design/openapi-contract-testing
```

Setup
-----

[](#setup)

### 1. Bundle your OpenAPI spec

[](#1-bundle-your-openapi-spec)

This package expects a **bundled** (all `$ref`s resolved) JSON spec file. Use [Redocly CLI](https://redocly.com/docs/cli/commands/bundle/) to bundle:

```
npx @redocly/cli bundle openapi/root.yaml --dereferenced -o openapi/bundled/front.json
```

> **Important:** The `--dereferenced` flag is required. Without it, `$ref` pointers (e.g., `#/components/schemas/...`) are preserved in the output, causing `UnresolvedReferenceException` at validation time. The underlying JSON Schema validator (`opis/json-schema`) does not resolve OpenAPI `$ref` references.

### 2. Configure PHPUnit extension

[](#2-configure-phpunit-extension)

Add the coverage extension to your `phpunit.xml`:

```

```

ParameterRequiredDefaultDescription`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

[](#3-use-in-tests)

#### With Laravel (recommended)

[](#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):

1. Method-level `#[OpenApiSpec]` attribute
2. Class-level `#[OpenApiSpec]` attribute
3. `openApiSpec()` method override
4. `config('openapi-contract-testing.default_spec')`

> **Note:** You can still override `openApiSpec()` as before — it remains fully backward-compatible.

#### Framework-agnostic

[](#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

[](#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
---------------

[](#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)

[](#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

[](#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

[](#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`:

```

```

Or via environment variable (takes priority over `phpunit.xml`):

```
OPENAPI_CONSOLE_OUTPUT=uncovered_only vendor/bin/phpunit
```

CI Integration
--------------

[](#ci-integration)

### GitHub Actions Step Summary

[](#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_file` configured, the Markdown report is written to both the file and the Step Summary.

### Markdown output file

[](#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:

```

```

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
------------------

[](#openapi-30-vs-31)

The package auto-detects the OAS version from the `openapi` field and handles schema conversion accordingly:

Feature3.0 handling3.1 handling`nullable: true`Converted to type array `["string", "null"]`Not applicable (uses type arrays natively)`prefixItems`N/AConverted to `items` array (Draft 07 tuple)`$dynamicRef` / `$dynamicAnchor`N/ARemoved (not in Draft 07)`examples` (array)N/ARemoved (OAS extension)`readOnly` / `writeOnly`Removed (OAS-only in 3.0)Preserved (valid in Draft 07)API Reference
-------------

[](#api-reference)

### `OpenApiResponseValidator`

[](#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`

[](#openapispecloader)

Manages spec loading and configuration.

```
OpenApiSpecLoader::configure('/path/to/bundled/specs', ['/api']);
$spec = OpenApiSpecLoader::load('front');
OpenApiSpecLoader::reset(); // For testing
```

### `OpenApiCoverageTracker`

[](#openapicoveragetracker)

Tracks which endpoints have been validated.

```
OpenApiCoverageTracker::record('front', 'GET', '/v1/pets');
$coverage = OpenApiCoverageTracker::computeCoverage('front');
// ['covered' => [...], 'uncovered' => [...], 'total' => 45, 'coveredCount' => 12]
```

Development
-----------

[](#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
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance87

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 98.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

13

Last Release

63d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6fc976536b6b21ad39ca38269c6c59eceabc5fd57d6627e6742279cff53c387a?d=identicon)[wadakatu](/maintainers/wadakatu)

---

Top Contributors

[![wadakatu](https://avatars.githubusercontent.com/u/72595463?v=4)](https://github.com/wadakatu "wadakatu (84 commits)")[![renovate[bot]](https://avatars.githubusercontent.com/in/2740?v=4)](https://github.com/renovate[bot] "renovate[bot] (1 commits)")

---

Tags

phpunitcoverageopenapiapi testingcontract-testing

###  Code Quality

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/studio-design-openapi-contract-testing/health.svg)

```
[![Health](https://phpackages.com/badges/studio-design-openapi-contract-testing/health.svg)](https://phpackages.com/packages/studio-design-openapi-contract-testing)
```

###  Alternatives

[brianium/paratest

Parallel testing for PHP

2.5k118.8M754](/packages/brianium-paratest)[ergebnis/phpunit-slow-test-detector

Provides facilities for detecting slow tests in phpunit/phpunit.

1468.1M72](/packages/ergebnis-phpunit-slow-test-detector)[hotmeteor/spectator

Testing helpers for your OpenAPI spec

3021.4M1](/packages/hotmeteor-spectator)[nimut/phpunit-merger

Merge multiple PHPUnit reports into one file

501.7M7](/packages/nimut-phpunit-merger)[robiningelbrecht/phpunit-pretty-print

Prettify PHPUnit output

76460.0k15](/packages/robiningelbrecht-phpunit-pretty-print)[ockcyp/covers-validator

Validates @covers tags in PHPUnit tests

21198.0k82](/packages/ockcyp-covers-validator)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
