Skip to content

Repository files navigation

verev

Fresh, modern consumer-driven contract testing for PHP.

verev lets a consumer's test describe exactly what it needs from a provider, produce a contract from that expectation, and have the provider verify it — so teams develop and deploy services independently without brittle, slow end-to-end tests.

  • Pure PHP. No FFI, no Rust core, no per-platform native binaries. Installs and behaves the same on glibc, Alpine/musl, and macOS.
  • One command to install. composer require --dev verev/verev.
  • HTTP and async messaging contracts.
  • First-class PHPUnit and Pest integration.
  • Git-native exchange by default — no broker to stand up.

Status: early (0.1.0). The consumer DSL, matching engine, mock server, provider verifier, git-backed store, PHPUnit/Pest bridges, and CLI are implemented and tested. See Status & limitations before adopting it for critical release gating.

Contents

The idea in 30 seconds

CONSUMER test ──► verev mock server ──► ./contracts/<provider>/<consumer>.verev.json
                                                     │
                                              git push / share
                                                     ▼
PROVIDER CI ── verev verify ──► replays the recorded requests against the real provider
                                                     │
                                          records pass/fail per consumer
                                                     ▼
RELEASE ── verev can-i-deploy ──► blocks unless every consumer contract is verified

The consumer owns the contract. The provider must satisfy it. Only what a consumer actually uses is ever tested.

Requirements

  • PHP 8.2+
  • ext-json, ext-curl
  • The mock server uses proc_open (available on Linux and macOS).

Install

composer require --dev verev/verev

Scaffold the project (creates ./contracts and prints next steps):

vendor/bin/verev init

Consumer tests

A consumer test declares the interactions it expects, points your real HTTP client at the mock server, and — if every expectation was met — a contract file is written.

PHPUnit (recommended)

Add the InteractsWithVerev trait to your test case:

use PHPUnit\Framework\TestCase;
use Verev\Consumer\ContractBuilder;
use Verev\Consumer\Dsl\Matchers;
use Verev\PHPUnit\InteractsWithVerev;

final class OrdersClientTest extends TestCase
{
    use InteractsWithVerev;

    public function test_it_fetches_an_order(): void
    {
        $contract = (new ContractBuilder('web', 'orders-api'))
            ->uponReceiving('a request for order 1')
                ->given('order 1 exists', ['id' => 1])
                ->withRequest('GET', '/orders/1', headers: ['Accept' => 'application/json'])
                ->willRespondWith(200, ['Content-Type' => 'application/json'], [
                    'id'     => Matchers::type(1),
                    'sku'    => Matchers::regex('^[A-Z]{3}-\d+$', 'ABC-1'),
                    'lines'  => Matchers::eachLike(['price' => Matchers::type(9.99)]),
                ])
            ->build();

        // Starts a real mock server on 127.0.0.1:<random-port>.
        $session = $this->mockProvider($contract);

        // Exercise the code under test against the mock server.
        $order = (new OrdersClient($session->baseUrl()))->find(1);
        $this->assertSame(1, $order->id);

        // Verifies every expectation was hit and writes the contract on success.
        // If an assertion above failed, execution never reaches here, so no
        // contract is written for a failing test.
        $this->assertContractIsFulfilled($session);
    }
}

By default the contract is written to ./contracts (relative to the working directory). Pass a directory to override: $this->mockProvider($contract, __DIR__ . '/contracts').

Pest

The Pest bridge is auto-registered when Pest is installed. It adds a verevMockProvider() helper and a toFulfilContract() expectation:

use Verev\Consumer\ContractBuilder;
use Verev\Consumer\Dsl\Matchers;

it('fetches an order', function () {
    $contract = (new ContractBuilder('web', 'orders-api'))
        ->uponReceiving('a request for order 1')
            ->withRequest('GET', '/orders/1')
            ->willRespondWith(200, body: ['id' => Matchers::type(1)])
        ->build();

    $session = verevMockProvider($contract);

    $order = (new OrdersClient($session->baseUrl()))->find(1);

    expect($order->id)->toBe(1)
        ->and($session)->toFulfilContract();
});

Any other runner

MockProviderSession is framework-agnostic:

use Verev\Testing\MockProviderSession;

$session = MockProviderSession::start($contract, __DIR__ . '/contracts');
// ... exercise your client against $session->baseUrl() ...
$errors = $session->finish();          // list<string>; empty means success
// $session writes the contract when $errors === []

Matchers

Embed matchers directly in request/response bodies. Each stores a concrete example (persisted in the contract) plus the rule used during verification.

Matcher Meaning
Matchers::type($example) Any value of the same type
Matchers::like($example) Alias of type, reads well for whole structures
Matchers::regex($pattern, $example) A string matching the regex (no delimiters)
Matchers::eachLike($template, $min = 1) A list of ≥ min elements, each matching the template
Matchers::equalTo($example) Exactly this value

Matchers work in request bodies too, so the mock server can tell apart two requests to the same URL that differ only by body.

Async messaging (consumer)

For message consumers there is no wire to exercise. verev generates an example message from the contract and feeds it to your real handler; if the handler accepts it, the interaction is recorded.

use Verev\Consumer\ContractBuilder;
use Verev\Consumer\Dsl\Matchers;
use Verev\PHPUnit\InteractsWithVerev;
// ... inside a TestCase using InteractsWithVerev ...

public function test_it_handles_an_order_placed_event(): void
{
    $builder = new ContractBuilder('billing', 'orders-events');

    $this->messageHarness($builder)->verify(
        'an order placed event',
        ['orderId' => Matchers::type('abc'), 'total' => Matchers::type(100)],
        handler: fn (array $message) => (new InvoiceListener())->handle($message),
        metadata: ['topic' => 'orders'],
    );

    // Message contracts have no mock server, so publish explicitly at the end.
    $this->publishContract($builder->build());
}

If the handler throws, MessageVerificationFailed is raised and nothing is written.

Provider verification

From the CLI

Point verev at a running provider and the directory of published contracts:

vendor/bin/verev verify --provider=orders-api --base-url=http://localhost:8080

It replays each recorded request, compares the response against the contract's matching rules, prints a per-interaction report, records the result, and exits non-zero on any failure.

From PHP (provider states & message producers)

For provider states (seeding data) or message verification, drive the verifier in code:

use Verev\Core\ProviderState;
use Verev\Provider\CurlHttpClient;
use Verev\Provider\ProviderStateHandler;
use Verev\Provider\Verifier;
use Verev\Store\FilesystemContractStore;

$stateHandler = new class implements ProviderStateHandler {
    public function supports(string $state): bool
    {
        return 'order 1 exists' === $state;
    }

    public function setUp(ProviderState $state): void
    {
        // seed the database, prime a cache, call a fixtures endpoint, ...
    }
};

$store = new FilesystemContractStore(__DIR__ . '/contracts');

$verifier = new Verifier(
    new CurlHttpClient(),
    baseUrl: 'http://localhost:8080',
    stateHandlers: [$stateHandler],
    // For async messages, register a producer per interaction description:
    messageProducers: [
        'an order placed event' => fn () => ['orderId' => 'abc', 'total' => 100],
    ],
);

foreach ($store->fetchForProvider('orders-api') as $contract) {
    $report = $verifier->verify($contract);
    // inspect $report->passed(), $report->failures()
}

Sharing contracts & gating deploys

Contracts are plain files under ./contracts. The default exchange is an ordinary git repository — no broker required.

# On the consumer's CI, after its tests wrote the contracts:
vendor/bin/verev publish --contracts=./contracts --message="web @ $(git rev-parse --short HEAD)" --push

# Before releasing the provider:
vendor/bin/verev can-i-deploy --provider=orders-api   # exits non-zero if unsafe

can-i-deploy fails safe: a missing or failed verification blocks the deploy. Teams that outgrow git can implement the ContractStore interface to target a hosted registry service instead.

CLI reference

verev init            Scaffold verev in the current project
verev verify          Verify a provider against its consumer contracts
verev publish         Publish contracts by committing them to git
verev can-i-deploy    Check whether a provider is safe to deploy
Command Key options
init --dir=.
verify --provider=NAME (required), --base-url=URL (required), --contracts=./contracts
publish --contracts=./contracts, --message="…", --push
can-i-deploy --provider=NAME (required), --contracts=./contracts

How it works

A contract is between one consumer and one provider and contains a list of interactions (http or message). It is serialized as ./contracts/<provider>/<consumer>.verev.json:

{
  "formatVersion": "1.0",
  "consumer": { "name": "web" },
  "provider": { "name": "orders-api" },
  "interactions": [
    {
      "type": "http",
      "description": "a request for order 1",
      "providerStates": [{ "name": "order 1 exists", "params": { "id": 1 } }],
      "request": { "method": "GET", "path": "/orders/1", "query": {}, "headers": {}, "body": null, "bodyRules": {} },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": { "id": 1 },
        "bodyRules": { "$.id": { "match": "type" } }
      }
    }
  ],
  "metadata": { "verevVersion": "0.1.0" }
}

Matching rules are a flat map of JSONPath-like path → rule, shared by HTTP bodies and message payloads, so one engine serves both.

Extending verev

Every seam is an interface — implement one and register it:

Interface Extends
Verev\Core\Interaction New interaction types (sync messages, gRPC, …)
Verev\Matching\Matcher New matching rules
Verev\Matching\Body\BodyMatcher New body content types
Verev\Store\ContractStore New exchange/registry backends
Verev\Provider\ProviderStateHandler Provider state setup
Verev\Consumer\MockServer\MockServer Alternative mock server

Runnable example

A self-contained end-to-end demo (consumer → contract → real provider → verify → can-i-deploy) lives in examples/:

php examples/end_to_end.php

Development

composer install
composer ci        # php-cs-fixer (dry-run) + phpstan (max) + phpunit + pest

Individual steps: composer cs, composer cs-fix, composer phpstan, composer test, composer test-pest.

Status & limitations

verev is an early 0.1.0. It is suitable for early adopters on internal services; it is not yet recommended as the release gate other teams depend on. Known limitations:

  • The mock server uses proc_open/sockets and handles one request at a time; Windows is not yet supported.
  • can-i-deploy is not yet environment/branch-aware (latest-write-wins per consumer).
  • Provider states and message producers are wired through the PHP API, not yet the CLI.
  • The contract format is versioned by constant; there is no published JSON Schema or migration tooling yet.

See the design document under docs/superpowers/specs/ for the full roadmap.

License

MIT

About

Fresh and modern consumer driven contract testing for PHP

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages