{"slug": "vcr-py-make-public-api-tests-repeatable-in-python", "title": "VCR.py: Make Public API Tests Repeatable in Python", "summary": "VCR.py records HTTP requests and responses during a test and replays them on later runs, letting Python tests reuse recorded API responses instead of calling a live API each time. The tutorial demonstrates the tool with vcrpy v8.3.0, requests v2.32.4, and pytest v9.1.1 against a client that reads repository metadata from the GitHub REST API, saving each request and response to a cassette file. The approach removes network, API server, and rate-limit dependencies that can cause tests to fail even when the code under test is correct.", "body_md": "Table of Contents\n\nIntroduction\n\nA good test keeps the moving parts small. If the inputs, dependencies, and environment stay the same, a failure is more likely to point to your code.\n\nThat is hard when your code depends on a public API. A public API adds things you do not control, such as the network, the API server, and rate limits.\n\nThat means the test can fail even when your code did nothing wrong.\n\nVCR.py helps by letting tests reuse recorded API responses instead of calling the live API every time.\n\nThis article shows how to use VCR.py to make API-dependent tests more repeatable.\n\n💻 Get the Code: The reproducible examples are in [notebooks/record-replay-api-tests-vcrpy](../notebooks/record-replay-api-tests-vcrpy/), including the GitHub API client, pytest tests, and recorded cassette.\n\n  Stay Current with CodeCut\n\n  Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.\n\n    .codecut-subscribe-wrap {\n\n        display: flex;\n\n        justify-content: center;\n\n    }\n\n    .codecut-subscribe-btn {\n\n        background: #72BEFA !important;\n\n        color: #2F2D2E !important;\n\n        border: none;\n\n        border-radius: 8px;\n\n        padding: 12px 28px;\n\n        font-family: inherit;\n\n        font-size: 16px;\n\n        font-weight: 700;\n\n        cursor: pointer;\n\n        text-decoration: none !important;\n\n        display: inline-flex;\n\n        align-items: center;\n\n        justify-content: center;\n\n        transition: background 0.3s ease;\n\n    }\n\n    .codecut-subscribe-btn:hover,\n\n    .codecut-subscribe-btn:focus {\n\n        background: #5aa8e8 !important;\n\n        color: #2F2D2E !important;\n\n        text-decoration: none !important;\n\n    }\n\n    /* Mobile responsive */\n\n    @media (max-width: 480px) {\n\n        .codecut-subscribe-btn {\n\n            width: 100%;\n\n            text-align: center;\n\n        }\n\n    }\n\nWhat Is VCR.py?\n\n[VCR.py](https://github.com/kevin1024/vcrpy) records HTTP requests and responses during a test, then replays them in later runs so the test no longer depends on the live API every time.\n\nThe workflow is simple:\n\nRun the test once and let it make the real HTTP request.\n\nVCR.py saves the request and response to a cassette file.\n\nLater test runs replay the saved response instead of calling the network.\n\nThe next sections show how this works with a practical pytest example.\n\nSetup\n\nInstall the libraries used in this tutorial:\n\npip install vcrpy requests pytest\n\nThis article uses vcrpy v8.3.0, requests v2.32.4, and pytest v9.1.1.\n\nWe will test a small client that reads repository metadata from the [GitHub REST API](https://docs.github.com/en/rest/repos/repos).\n\nCreate a file named repo_client.py that does the following:\n\nMakes an external HTTP request.\n\nParses nested JSON.\n\nDepends on fields controlled by another service.\n\nimport requests\n\ndef get_repository_summary(owner: str, repo: str) -> dict[str, str | int | None]:\n\n    # Call the live GitHub API.\n\n    url = f\"https://api.github.com/repos/{owner}/{repo}\"\n\n    response = requests.get(url, timeout=10)\n\n    response.raise_for_status()\n\n    # Parse the JSON fields the app needs.\n\n    data = response.json()\n\n    license_data = data.get(\"license\")\n\n    # Return a smaller, app-specific summary.\n\n    return {\n\n        \"full_name\": data[\"full_name\"],\n\n        \"description\": data[\"description\"],\n\n        \"stars\": data[\"stargazers_count\"],\n\n        \"license\": license_data[\"spdx_id\"] if license_data else None,\n\n        \"default_branch\": data[\"default_branch\"],\n\n    }\n\nFor this example, the test should answer two questions:\n\nDid the request reach the right repository endpoint?\n\nDid the function extract the fields correctly from the JSON response?\n\nWrite the Live API Test\n\nFirst, write the test without VCR.py. It calls the live GitHub API and verifies that the client returns the expected repository summary.\n\nfrom repo_client import get_repository_summary\n\ndef test_get_repository_summary_live():\n\n    summary = get_repository_summary(\"kevin1024\", \"vcrpy\")\n\n    assert summary[\"full_name\"] == \"kevin1024/vcrpy\"\n\n    assert summary[\"license\"] == \"MIT\"\n\n    assert summary[\"default_branch\"] == \"master\"\n\nWhile the test passes, the future test runs can fail because:\n\nGitHub is unavailable.\n\nYour machine has no network access.\n\nThe request takes too long.\n\nThe API returns a temporary error.\n\nA failed test becomes hard to interpret: did the client code break, or did the API fail to respond? At that point, the test is no longer only testing the client code.\n\nVCR.py addresses this by turning the API response into a local fixture. The test can keep checking the client code without depending on whether GitHub responds.\n\nRecord the API Response Once\n\nFirst, import VCR.py and the function you want to test:\n\nimport vcr\n\nfrom repo_client import get_repository_summary\n\nNext, create a VCR configuration for this test file:\n\n# Store cassettes in a predictable test folder.\n\ngithub_vcr = vcr.VCR(\n\n    cassette_library_dir=\"tests/fixtures/cassettes\",\n\n    record_mode=\"once\",\n\n)\n\nWith once, VCR.py records only when it needs to create the cassette:\n\nIf the cassette does not exist, VCR.py calls the API and records the response.\n\nIf the cassette exists, VCR.py replays the matching response from the cassette.\n\nNext, use use_cassette to tell VCR.py which cassette file this test should record to and replay from:\n\n# Record this request once, then replay it later.\n\n@github_vcr.use_cassette(\"github_vcrpy.yaml\")\n\ndef test_get_repository_summary_with_vcr():\n\n    summary = get_repository_summary(\"kevin1024\", \"vcrpy\")\n\nRun it the same way you run any pytest test:\n\npytest tests/test_repo_client.py\n\nOn the first run, VCR.py does not have a cassette yet. It lets the request go to GitHub, captures the response, and writes it to tests/fixtures/cassettes/github_vcrpy.yaml.\n\nThe cassette contains the recorded HTTP interaction. A shortened version looks like this:\n\ninteractions:\n\n- request:\n\n    method: GET\n\n    uri: https://api.github.com/repos/kevin1024/vcrpy\n\n  response:\n\n    status:\n\n      code: 200\n\n      message: OK\n\n    body:\n\n      string: '{\"id\": 3736670, \"name\": \"vcrpy\", ...}'\n\nversion: 1\n\nSee the [full cassette on GitHub](https://github.com/khuyentran1401/codecut-blog/blob/main/record-replay-api-tests-vcrpy/tests/fixtures/cassettes/github_vcrpy.yaml) for the complete recorded request and response.\n\nOnce the cassette exists, VCR.py can use it on future test runs.\n\nReplay the Test Offline\n\nRun the same test again:\n\npytest tests/test_repo_client.py\n\nThis time, VCR.py sees the cassette file. Instead of sending another request to GitHub, it replays the saved response.\n\nThis gives you a more stable API test:\n\nThe client still parses a real GitHub response, so the test uses real response data.\n\nThe test no longer needs GitHub on every run, so temporary external issues do not break it.\n\nIf you refresh the cassette, Git shows the recorded response changes for review.\n\nControl When Cassettes Change\n\nBy default, VCR.py can record a cassette when one is missing. That is useful locally, where you can inspect the new file before committing it.\n\nIn a [GitHub Actions workflow](https://codecut.ai/run-github-actions-locally-act/), that is risky because the build can create or update a fixture without review, making a passing test harder to trust.\n\nTo prevent CI from recording new API responses, set record_mode=\"none\" in the CI environment:\n\nimport vcr\n\nci_vcr = vcr.VCR(\n\n    cassette_library_dir=\"tests/fixtures/cassettes\",\n\n    record_mode=\"none\",\n\n)\n\nWith none, VCR.py only replays existing cassettes:\n\nIf the cassette exists, VCR.py replays matching requests from the cassette.\n\nIf the cassette is missing, the test fails.\n\nIf your code makes a new unmatched request, the test fails.\n\nUse the VCR instance above in your test:\n\nfrom repo_client import get_repository_summary\n\n@ci_vcr.use_cassette(\"github_vcrpy.yaml\")\n\ndef test_get_repository_summary_in_ci():\n\n    summary = get_repository_summary(\"kevin1024\", \"vcrpy\")\n\nWith this configuration, CI can only use responses that were already recorded and committed.\n\nIf the API response needs to change, you can re-record the cassette locally, inspect the Git diff, and commit that update intentionally.\n\nVCR.py vs Mocking\n\nFor those who are familiar with mocking, you might be wondering: “Why should I use VCR.py instead of mocking?”\n\nThe key difference is the source of the response: mocking replaces the API with a fake response, while VCR.py replays a response recorded from the real API.\n\nThe code below shows a test that uses mocking to control the API response:\n\nfrom unittest.mock import Mock, patch\n\ndef get_repository_license(owner: str, repo: str) -> str | None:\n\n    response = requests.get(f\"https://api.github.com/repos/{owner}/{repo}\")\n\n    response.raise_for_status()\n\n    data = response.json()\n\n    return data[\"license\"][\"spdx_id\"] if data[\"license\"] else None\n\n@patch(\"requests.get\")\n\ndef test_get_repository_license_with_mock(mock_get):\n\n    mock_response = Mock()\n\n    mock_response.json.return_value = {\"license\": {\"spdx_id\": \"MIT\"}}\n\n    mock_response.raise_for_status.return_value = None\n\n    mock_get.return_value = mock_response\n\nlicense_id = get_repository_license(\"kevin1024\", \"vcrpy\")\n\nassert license_id == \"MIT\"\n\nIn this test:\n\n@patch(\"requests.get\") replaces the real HTTP call during the test.\n\nmock_response acts like the response object requests.get() would normally return.\n\nmock_response.json.return_value defines the JSON payload used in the test.\n\nThe assertion checks that the test-provided payload leads to \"MIT\".\n\nThis graph shows the difference between a mock and VCR.py:\n\nSo why should you use VCR.py instead of mocking? [Mocks](https://codecut.ai/pytest-for-data-scientists/) are useful when the goal is to isolate your code. For example, this mock forces a successful response:\n\nmock_response.json.return_value = {\"license\": {\"spdx_id\": \"MIT\"}}\n\nmock_response.raise_for_status.return_value = None\n\nThat is a good unit test, but it does not check the real HTTP interaction. A public API can also return status codes and headers that affect your client:\n\nStatus: 403\n\nX-RateLimit-Remaining: 0\n\nFor that kind of integration-style test, VCR.py is a better fit because it records the real HTTP response once and replays it later.\n\nA good testing stack often uses all three layers:\n\nLayer\n\nPurpose\n\nMocked unit tests\n\nFast checks for your own logic\n\nVCR.py tests\n\nDeterministic tests against recorded real HTTP responses\n\nA few live smoke tests\n\nConfirmation that the external service still behaves as expected\n\nA Practical VCR.py Testing Workflow\n\nSome good practices when using VCR.py:\n\nCommit cassettes with the tests that use them, so every test has the response fixture it needs.\n\nRun CI in replay-only mode, so builds cannot create or update fixtures without review.\n\nReview cassette diffs before committing them, so response changes do not get added to your tests unnoticed.\n\nThat keeps API tests stable without hiding external changes.\n\n📚 For more context on building a complete testing and CI workflow, see [Production-Ready Data Science](https://codecut.ai/production-ready-data-science/).\n\nReferences\n\n[VCR.py API docs](https://vcrpy.readthedocs.io/en/latest/api.html) (VCR.py docs, 2026): Configuration options including record_mode, match_on, serializer, and cassette_library_dir.\n\n[VCR.py usage docs](https://vcrpy.readthedocs.io/en/latest/usage.html) (VCR.py docs, 2026): Record modes including once, new_episodes, none, and all.\n\nThe post [VCR.py: Make Public API Tests Repeatable in Python](https://codecut.ai/vcrpy-repeatable-public-api-tests-python/) appeared first on [CodeCut](https://codecut.ai).", "url": "https://wpnews.pro/news/vcr-py-make-public-api-tests-repeatable-in-python", "canonical_source": "https://codecut.ai/vcrpy-repeatable-public-api-tests-python/", "published_at": "2026-09-08 14:58:55+00:00", "updated_at": "2026-09-22 10:22:21.394170+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["VCR.py", "Python", "pytest", "requests", "GitHub REST API", "kevin1024"], "alternates": {"html": "https://wpnews.pro/news/vcr-py-make-public-api-tests-repeatable-in-python", "markdown": "https://wpnews.pro/news/vcr-py-make-public-api-tests-repeatable-in-python.md", "text": "https://wpnews.pro/news/vcr-py-make-public-api-tests-repeatable-in-python.txt", "jsonld": "https://wpnews.pro/news/vcr-py-make-public-api-tests-repeatable-in-python.jsonld"}}