When testing code that calls external APIs, you usually choose between real HTTP calls (slow and depend on third-party services), and hand-written mocks (tedious and often differ from what the API actually returns). Libraries like php-vcr popularized a third option: record the real exchanges once and replay them in your tests. Symfony 8.2 adds this feature natively to the HttpClient component.
Enabling the Recorder
The new RecorderHttpClient decorates the HTTP client transport, so it covers
both the default client and your scoped clients. It's driven by the SymfonyExtension
of the PHPUnit Bridge, which the Symfony Flex recipe registers in phpunit.dist.xml.
You only have to enable the recorder in the test environment:
1 2 3 4
# config/packages/http_client.yaml
when@test:
http_client:
recorder: true
Recording and Replaying Your Tests
Add the #[UseRecord] attribute to the tests that call external APIs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// tests/Service/ExchangeRatesTest.php
namespace App\Tests\Service;
use App\Service\ExchangeRates;
use Symfony\Bridge\PhpUnit\Attribute\UseRecord;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ExchangeRatesTest extends KernelTestCase
{
#[UseRecord]
public function testGetRate(): void
{
$exchangeRates = static::getContainer()->get(ExchangeRates::class);
$this->assertSame(1.08, $exchangeRates->getRate('EUR', 'USD'));
}
}
Run the test once with the SYMFONY_HTTP_RECORDER environment variable to make
the real requests and record them (the missing value is explained later):
1
$ SYMFONY_HTTP_RECORDER=missing vendor/bin/phpunit --filter testGetRate
This creates tests/Service/ExchangeRatesTest/testGetRate.har using the standard
HAR format, which is human-readable, easy to review in pull requests and supported
by browser dev tools. Commit this file; from now on, all test runs (including CI)
replay the responses from it. If a request isn't found in the file, Symfony throws
a HarEntryNotFoundException instead of reaching the network.
Controlling What Gets Recorded
The attribute only selects the file. Whether a test can reach the network is decided
when running the tests, so you can't commit a test that makes real requests by
mistake. The SYMFONY_HTTP_RECORDER environment variable accepts these values:
replay(the default): replays the responses and fails when a request is not in the file;missing: replays the responses, and makes and records only the requests not in the file;record: always makes the real requests and rewrites the files of the tests that run (useful to refresh them after an API change);passthrough: makes the real requests without reading or writing the files (useful to check your tests against the live services).
Tests without #[UseRecord] aren't affected: their requests are sent as usual.
Requests are matched on their method, URL and body, and repeated ones are replayed
in the order they were recorded, so sequences like polling an API until a job
finishes also work.
Keeping Secrets Out of Recordings
HAR files are committed to your repository, so they must not contain credentials.
By default, the recorder replaces secrets in requests and responses with
[REDACTED] before writing anything to disk. This includes headers such as
Authorization, Cookie and X-Api-Key, and query string, form and JSON
fields such as password, api_key, client_secret and access_token.
Requests are also compared with their secrets masked, so they still match when
replayed with real credentials. Use the redact option to mask more names and
redact_except to remove names from the built-in lists:
1 2 3 4 5 6
# config/packages/http_client.yaml
when@test:
http_client:
recorder:
redact: ['X-Custom-Secret', 'nonce']
redact_except: ['code']
Masking a field that changes on every request, like nonce here, also excludes
it from request matching. For full control, set the redactor option to the ID
of a service that implements RedactorInterface.
Note
Replayed responses contain the masked values, so if your code validates something
like the signature of an id_token, you'll need a custom redactor.