Skip to content

VCR bridge #266

Description

@roxblnfk

testo/bridge-vcr — API

PHP-VCR integration: mark a test with #[VCR] and its HTTP interactions are recorded to a cassette on the first run and replayed offline afterwards.

Install

composer require --dev testo/bridge-vcr

#[VCR] attribute — Testo\Bridge\VCR

Targets a method or a class (class-level = default for every test in the case; a method-level attribute overrides it). It is self-wiring — the interceptor is inserted into the pipeline only for tagged tests, so no plugin registration is needed.

public function __construct(
    string $name,                 // cassette file name (required)
    ?RecordMode $mode = null,     // null → inherit php-vcr default (NewEpisodes)
    array $match = [],            // list<Matcher>; empty → php-vcr default (method + url)
)

RecordModeTesto\Bridge\VCR\RecordMode

Case php-vcr mode Behaviour
NewEpisodes new_episodes Replay existing; record anything new (real call). Default.
Once once Record on first run (no cassette file yet), replay-only after.
None none Replay-only; a request with no match throws. Best for CI.

MatcherTesto\Bridge\VCR\Matcher

Which request attributes must line up for a recording to replay. Transport-agnostic (php-vcr normalizes stream_wrapper/curl/soap into one request object).

Method, Url (path + query; there is no separate path), Host, QueryString, Body, PostFields (form POSTs only), Headers, SoapOperation (SOAP envelope only).

VcrPlugin — optional

#[VCR] works on its own. Register Testo\Bridge\VCR\VcrPlugin only to point php-vcr at a non-default cassette directory:

// testo.php
use Testo\Application\Config\ApplicationConfig;
use Testo\Application\Config\SuiteConfig;
use Testo\Bridge\VCR\VcrPlugin;

return new ApplicationConfig(
    plugins: [new VcrPlugin(cassettePath: __DIR__ . '/tests/fixtures')],
    suites:  [new SuiteConfig(name: 'Feature', location: ['tests/Feature'])],
);

VcrPlugin(?string $cassettePath = null)null keeps php-vcr's default (tests/fixtures, relative to CWD); global in php-vcr, so the last configured suite wins; the directory must exist.

Examples

use Testo\Bridge\VCR;
use Testo\Bridge\VCR\Matcher;
use Testo\Bridge\VCR\RecordMode;

// Replay-only, default matchers (method + url):
#[VCR('github-user', mode: RecordMode::None)]
public function fetchesUser(): void
{
    $json = \file_get_contents('https://api.github.com/users/roxblnfk');
    // assert on $json …
}

// Match on body too (e.g. a JSON API POST):
#[VCR('checkout', mode: RecordMode::None, match: [Matcher::Method, Matcher::Url, Matcher::Body])]
public function submitsOrder(): void { /* … */ }
// Class-level default cassette for the whole case; a method may override it:
#[VCR('users-api', mode: RecordMode::None)]
final class UsersApiTest
{
    public function listsUsers(): void { /* … */ }              // uses users-api
    public function getsOne(): void { /* … */ }                  // uses users-api

    #[VCR('user-detail', mode: RecordMode::None)]
    public function getsDetail(): void { /* … */ }               // overrides → user-detail
}

Layout

Package-level attribute like plugin/retry: Testo\Bridge\VCR lives at the package root (VCR.php, loaded via composer files); everything else is under Testo\Bridge\VCR\ in src/.

Concurrency semantics

php-vcr is process-global (one active cassette per process). A #[VCR] test therefore runs as an exclusive, synchronous block: while the cassette is inserted the test does not yield to the fiber scheduler, two #[VCR] tests never overlap, and awaiting real async work inside the window is unsupported by design.

Note

Released php-vcr constrains symfony/event-dispatcher to ^4–^7, so it can't install on Symfony 8 yet. Symfony 8 support is in php-vcr #442 (unreleased). The bridge is unblocked once php-vcr tags a Symfony 8 release.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions