Skip to main content

VCR for subprocess - record and replay subprocess calls for testing

Project description

Subprocess VCR

A Video Cassette Recorder (VCR) for subprocess commands that dramatically speeds up test execution by recording and replaying subprocess calls.

Quick Start

import subprocess
import pytest

# Mark test to use VCR - that's it!
@pytest.mark.subprocess_vcr
def test_with_vcr():
    result = subprocess.run(["echo", "hello"], capture_output=True, text=True)
    assert result.stdout == "hello\n"

Run tests:

# Record new interactions, replay existing ones
pytest --subprocess-vcr=record

# Replay only - fails if subprocess call not in cassette (for CI)
pytest --subprocess-vcr=replay

Recording Modes

Subprocess VCR supports several recording modes:

  • record - Replays existing recordings, records new ones. For each subprocess call, it first checks if a recording exists. If found, it replays that recording. If not found, it executes and records the new subprocess call. Useful for incremental test development.

  • replay - Replay only. Fails if a subprocess call is not found in the cassette. Ensures deterministic test execution in CI.

  • reset - Always record, replacing any existing cassettes and their metadata. Use this to refresh all recordings or when library behavior has changed.

  • replay+reset - Attempts to replay from existing cassettes, but on any test failure or missing recording, automatically retries the ENTIRE test in reset mode. This has the benefit over reset of only resetting the cassette where necessary: when replay succeeds, the existing cassette and metadata are preserved.

  • disable - No VCR, subprocess calls execute normally (default).

Filters for Normalization and Redaction

Subprocess VCR provides a powerful filter system to normalize dynamic values and redact sensitive information in your recordings. This ensures cassettes are portable, secure, and deterministic.

Built-in Filters

PathFilter

Normalizes filesystem paths that change between test runs, including paths relative to the current working directory:

from subprocess_vcr.filters import PathFilter

# Default normalization (pytest paths, home dirs, CWD, etc.)
@pytest.mark.subprocess_vcr(filters=[PathFilter()])
def test_with_paths():
    # Pytest temp paths
    subprocess.run(["ls", "/tmp/pytest-of-user/pytest-123/test_dir"])
    # Recorded as: ["ls", "<TMP>/test_dir"]

    # Current working directory paths
    subprocess.run(["cat", "/home/user/project/data/file.txt"], cwd="/home/user/project")
    # Recorded as: ["cat", "<CWD>/data/file.txt"] with cwd: "<CWD>"

# Custom path replacements
filter = PathFilter(replacements={
    r"/opt/myapp": "<APP_ROOT>",
    r"/var/log/\w+": "<LOG_DIR>",
})

RedactFilter

Removes sensitive information:

from subprocess_vcr.filters import RedactFilter

# Redact by patterns
filter = RedactFilter(
    patterns=[r"api_key=\w+", r"Bearer \w+"],
    env_vars=["API_KEY", "DATABASE_URL"],
)

@pytest.mark.subprocess_vcr(filters=[filter])
def test_with_secrets():
    subprocess.run(["curl", "-H", "Authorization: Bearer abc123"])
    # Recorded as: ["curl", "-H", "Authorization: <REDACTED>"]

Combining Filters

Using Multiple Filters

Filters are applied in order:

@pytest.mark.subprocess_vcr(filters=[
    PathFilter(),  # Handles all path normalization including CWD
    RedactFilter(env_vars=["API_KEY", "DATABASE_URL"]),
])
def test_complex_command():
    subprocess.run(["docker", "build", "-t", "myapp:latest", "."])

Global Configuration

Set filters for all tests in conftest.py:

@pytest.fixture(scope="session")
def subprocess_vcr_config():
    return {
        "filters": [
            PathFilter(),  # Handles all path normalization
            RedactFilter(env_vars=["API_KEY"]),
        ]
    }

Creating Custom Filters

Inherit from BaseFilter:

from subprocess_vcr.filters import BaseFilter

class MyCustomFilter(BaseFilter):
    def before_record(self, interaction: dict) -> dict:
        """Modify interaction before saving to cassette."""
        # Example: normalize custom IDs in output
        if interaction.get("stdout"):
            interaction["stdout"] = re.sub(
                r"request-id: \w+",
                "request-id: <REQUEST_ID>",
                interaction["stdout"]
            )
        return interaction

    def before_playback(self, interaction: dict) -> dict:
        """Modify interaction when loading from cassette."""
        # Usually just return unchanged
        return interaction

# Use the custom filter
@pytest.mark.subprocess_vcr(filters=[MyCustomFilter()])
def test_with_custom_filter():
    subprocess.run(["myapp", "process"])

VCR Context in Test Reports

When tests fail while using subprocess VCR, pytest shows additional context in the test report to help with debugging:

----------------------------- subprocess-vcr -----------------------------
This test replayed subprocess calls from VCR cassette: test_example.yaml
To re-record this test, run with: --subprocess-vcr=reset

This context appears for ANY test failure when VCR is replaying, helping you understand whether the failure might be due to outdated recordings.

Example Cassette

version: "1.0"
interactions:
  - args:
      - echo
      - hello world
    kwargs:
      stdout: PIPE
      stderr: PIPE
      text: true
    duration: 0.005
    returncode: 0
    stdout: |
      hello world
    stderr: ""
    pid: 12345

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

subprocess_vcr-0.1.0.tar.gz (57.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

subprocess_vcr-0.1.0-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file subprocess_vcr-0.1.0.tar.gz.

File metadata

  • Download URL: subprocess_vcr-0.1.0.tar.gz
  • Upload date:
  • Size: 57.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.8.11

File hashes

Hashes for subprocess_vcr-0.1.0.tar.gz
Algorithm Hash digest
SHA256 323771e8755ce67ec889c31e12b5a1b7892c7ad97397b4cae0752a6db2ab3317
MD5 851a368fddec5ff5025f9c9a7e35e714
BLAKE2b-256 1ca0796b850f9fb6bb8c148130887f4dde565fa717f39686c706fdef55ffd515

See more details on using hashes here.

File details

Details for the file subprocess_vcr-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for subprocess_vcr-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c4c2e663a7cd8866b54aa4a267205e8b4f5554d1f08604ec8834b18eda5ae86a
MD5 94c04a98108500f0ccc9c4bc90146e79
BLAKE2b-256 aacb3bddbdb1cfb89dd346a42fe073d6c53f69fdbaa88fa0125d26000810c373

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page