Skip to content

Use it with pytest

Cassetter ships with a pytest plugin. It is installed and activated automatically, you don't need to configure anything.

Mark a test

Add the @pytest.mark.vcr marker to any test that makes HTTP requests:

import httpx
import pytest


@pytest.mark.vcr
async def test_api_call():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.example.com/users")
    assert response.status_code == 200

Each marked test gets its own cassette file at:

{test_dir}/cassettes/{test_file_stem}/{test_name}.yaml

For a test test_api_call in tests/test_users.py, that is tests/cassettes/test_users/test_api_call.yaml.

For tests inside a class, the class name is included: TestUsers.test_api_call.yaml.

Record the cassette

By default the plugin runs in the none record mode: replay only, never touch the network. To record cassettes for the first time, pass --record-mode:

$ pytest --record-mode=once

After that, run pytest normally and the tests replay from the cassettes.

Tip

Keeping none as the default is intentional. Your test suite will fail loudly if a cassette is missing or stale, instead of silently making real requests in CI.

You can read about all the modes in Record modes.

Access the cassette

If you need to inspect the recorded interactions, request the cassette fixture:

from cassetter import Cassette


@pytest.mark.vcr
async def test_with_cassette(cassette: Cassette):
    async with httpx.AsyncClient() as client:
        await client.get("https://api.example.com/users")
    assert len(cassette.interactions) == 1

The fixture is also available under the name vcr, for compatibility with pytest-recording.

The cassette exposes vcrpy's introspection surface, so wire-contract assertions port over unchanged: cassette.requests returns the recorded requests with .method, .uri, .headers, .body, .path, .host, and .query attributes, and cassette.play_count, cassette.play_counts, and cassette.all_played report replay progress.

@pytest.mark.vcr
async def test_sends_tool_definitions(cassette: Cassette):
    ...
    request_body = json.loads(cassette.requests[0].body)
    assert request_body["tools"][0]["name"] == "get_weather"

Configure with vcr_config

Override the vcr_config fixture to set options for a whole module:

import pytest


@pytest.fixture(scope="module")
def vcr_config():
    return {
        "record_mode": "once",
        "match_on": ["method", "uri", "json_body"],
        "filter_headers": ["x-custom-secret"],
        "ignore_hosts": ["*.googleapis.com"],
    }

The supported keys are the same options accepted by use_cassette():

Key Description
record_mode Recording behavior, see Record modes
match_on Fields used to match requests
ignore_json_paths JSON paths ignored during matching
filter_headers Headers stripped from cassettes
filter_query_parameters Query params replaced in cassettes
body_scrub_patterns Body field patterns scrubbed from cassettes
filter_replacement Replacement string for filtered values
cassette_dir Cassette directory, relative to the test file
cassette_library_dir Cassette directory, used as is
intercept Libraries to intercept
max_age Cassette expiry, e.g. "30d"
on_expiry What to do with expired cassettes
ignore_localhost Bypass requests to localhost
ignore_hosts Bypass requests to matching hosts
before_record_request Hook to modify or skip requests
before_record_response Hook to modify or skip responses

The fixture can also return a Cassetter, which is the same set of options as an object, shareable with code that calls use_cassette() directly:

from cassetter import Cassetter


@pytest.fixture(scope="module")
def vcr_config() -> Cassetter:
    return Cassetter(record_mode="once", filter_headers=["x-custom-secret"])

Configure per test

The marker accepts overrides for a single test:

@pytest.mark.vcr("custom_name.yaml", record_mode="all")
async def test_special_case():
    ...

The first positional argument sets the cassette file name. The keyword arguments record_mode, cassette_dir, max_age, and on_expiry override the module configuration.

The command line flag --record-mode overrides everything.

Customize the cassette directory

Override the vcr_cassette_dir fixture:

@pytest.fixture(scope="module")
def vcr_cassette_dir():
    return "tests/my_cassettes"

Find orphaned cassettes

Over time, tests get renamed and deleted, and their cassettes stay behind. Find cassette files that no test uses:

$ pytest --vcr-check-orphans=tests/cassettes/