Pytest Marker
The main tool offered by this plugin to delete cassettes on failure is the pytest.mark.vcr_delete_on_fail()
decorator.
Automatic naming
./cassettes/{module-name}/{test-class-if-any.}{test_name}.yaml
import pytest
import requests
# Configure pytest_recording
@pytest.fixture(scope="module")
def vcr_config():
return {"record_mode": ["once"]}
# this will trigger pytest-recording and record a vcr
@pytest.mark.vcr
# this will delete that cassette, if the test fails
@pytest.mark.vcr_delete_on_fail
# @pytest.mark.vcr_delete_on_fail() # alternative syntax
def test_this():
requests.get("https://github.com")
assert False
Note
When using the marker decorator, the cassette will be deleted after the test teardown phase. This is different from how the context managers work.
Warning
This marker will delete any cassette found at the given path, even if the test didn’t reach the instruction that would have recorded a new cassette in the run that resulted in a failure. This is intended to ensure a fresh environment after every failure.
The marker is actually quite flexible and accepts several arguments (the full signature can be found
in the API Reference: pytest.mark.vcr_delete_on_fail()
).
Multiple cassettes
More than one cassette can be specified by passing in a list:
my_vcr = vcr.VCR(record_mode="once")
def get_cassette_name(letter: str) -> str:
return f"{letter}.yaml"
@pytest.mark.vcr_delete_on_fail(["a.yaml", get_cassette_name("b")])
def test_this():
with my_vcr.use_cassette("a.yaml"):
requests.get("https://github.com")
with my_vcr.use_cassette(get_cassette_name("b")):
requests.get("https://gitlab.com")
assert False
Note
The marker argument list element get_cassette_name("b")
will execute immediately at test collection time and
will result in a simple string passed as list element.
Delete cassettes programmatically
The cassette can also be determined by functions that will run after the test teardown:
from _pytest.python import Function
# setup vcr
my_vcr = vcr.VCR(record_mode="once")
def get_cassette(node: Function) -> str:
# the Function argument is a pytest node that has several information about the test
# and can be used to programmatically build the cassette path
test_name = node.name
return f"{test_name}.yaml"
# a function can be passed as target as a list element. It will run after the test teardown
@pytest.mark.vcr_delete_on_fail([get_cassette])
# @pytest.mark.vcr_delete_on_fail(target=get_cassette) # alternative syntax
# @pytest.mark.vcr_delete_on_fail.with_args(get_cassette) # alternative syntax
def test_this(request):
# This node is the same Function instance that 'get_cassette' will get as argument later
node: Function = request.node
test_name = node.name
with my_vcr.use_cassette(f"{test_name}.yaml"):
requests.get("https://github.com")
assert False
Warning
A function can’t be passed directly as the only unnamed marker argument. This is why the syntax
@pytest.mark.vcr_delete_on_fail(get_cassette)
will not work (and will probably means that the test will not
even be detected).
Functions and lists can be nested arbitrarily: all str
will be extracted and treated as paths
of cassettes to be deleted.
The correct type for a valid target
can be found in the Api Reference:
pytest_vcr_delete_on_fail.ValidTarget
.
Skip cassette deletion
It’s possible to selectively skip a cassette deletion, which could come in handy to inspect its content:
# pytest markers propagate to all test in a class
@pytest.mark.vcr
@pytest.mark.vcr_delete_on_fail
class TestCollection:
# this test cassette will be delete, since the test failed
def test_this(self):
requests.get("https://github.com")
assert False
# this test cassette will remain on disk despite the test failure
@pytest.mark.vcr_delete_on_fail(skip=True)
# @pytest.mark.vcr_delete_on_fail(None) # equivalent to the above
def test_this_as_well(self):
requests.get("https://github.com")
assert False
About pytest fixtures
Other than in the test itself, this marker detects failures in every setup / teardown pytest function scoped fixtures.
import pytest
import requests
@pytest.fixture
def setup_fixture():
raise Exception
yield
@pytest.fixture
def teardown_fixture():
yield
raise Exception
@pytest.mark.vcr
@pytest.mark.vcr_delete_on_fail
def test_one(setup_fixture):
assert requests.get("https://github.com").status_code == 200
@pytest.mark.vcr
@pytest.mark.vcr_delete_on_fail
def test_two(teardown_fixture):
assert requests.get("https://github.com").status_code == 200
Both these tests would result in no cassette saved on disk.