kiwicom / pytest-recording

A pytest plugin that allows recording network interactions via VCR.py
MIT License
445 stars 35 forks source link

Write multiple tests to the same cassette #42

Open joshtemple opened 4 years ago

joshtemple commented 4 years ago

Is it possible to write multiple tests to the same cassette? If multiple tests are making the same API calls, it's inefficient to record them again and again, better to use a shared cassette.

This is possible with vcr.use_cassette, but doesn't seem to be possible with pytest.mark.vcr, since the default cassette name is always the test name.

For example:

@pytest.mark.vcr("tests/cassettes/shared_cassette.yaml")
def test_a(): ...

@pytest.mark.vcr("tests/cassettes/shared_cassette.yaml")
def test_b(): ...

In this example, you will still write new cassettes to tests/cassettes/test_a.yaml and tests/cassettes/test_b.yaml, rather than using the shared one.

DevilXD commented 4 years ago

From what I can see by studying the code, you might be able to influence the "test function name -> file name" linking behavior by specifying a def default_cassette_name(request): fixture in your conftest.py. I am not entirely sure on this though, here is the source line that suggests this: https://github.com/kiwicom/pytest-recording/blob/ffe27e5aa7c78dbc0d1eff012335704852cc0d27/src/pytest_recording/plugin.py#L109

On L57, you can see the default vcr_config fixture that the docs suggest to overwrite the same way, so it may be worth a try.

Stranger6667 commented 4 years ago

Indeed, a combination of new_episodes record mode and custom default_cassette_name should work, and unique requests (according to used matchers) will be written in the same cassette file. As I remember that this behavior was chosen to avoid ambiguity on request/response pairs distribution among multiple cassettes - so it always writes to the "default" cassette, which is indeed not always convenient. For example:

@pytest.mark.vcr("gitlab_api_success.yaml", "slack_api_success.yaml")
def test_something():
    requests.get(...)  # Gitlab API call
    requests.post(...)  # Slack API call
    requests.pos(...)  # Some test-specific network call

In this case, it is not unambiguously clear which request/response pair should go to what cassette. Or at least it might be clear for the developer, but I decided to avoid any kind of assumptions from the library point of view. Nevertheless, I will be happy to improve user experience with recording cassettes and open to suggestions. What do you think if we will have a special CLI option that will define the name of the cassette to record? E.g. `--record-to=shared.yaml"

DevilXD commented 4 years ago

I think it'd be just fine to make it always write to the first cassette specified, while reading from all specified. If someone would need more control than that instead, I suggest making it possible to specify cassette fixtures as test arguments, roughly like pytest currently does with parametrization.

Reference: https://docs.pytest.org/en/latest/example/parametrize.html#parametrizing-conditional-raising (follow the link there for argument inputs types too)

Example:

@pytest.mark.vcr(gitlab_cassette="gitlab.yaml", slack_cassette="slack.yaml")
def test_apis(gitlab_cassette, slack_cassette):  # specifying more than one cassette kwarg enforces having to use the context manager yourself
    with gitlab_cassette:  # specify exactly what cassette you want to use here
        requests.get(...)
    with slack_cassette:
        requests.post(...)

This would also allow on custom operations being done on a cassette, such as using .rewind() - I'm not aware of an easy possibility of being able to do that right now.

Other possible usages:

@pytest.mark.vcr("main.yaml")  # just the cassette name, no kwarg
def test_apis(cassette):  # default fixture name giving access to the cassette itself
    # not specifying the cassette name as a kwarg means the entire test is implicatively wrapped just like before
    requests.get(...)
    cassette.rewind()  # possible given the cassette itself now
    requests.get(...)
@pytest.mark.vcr(my_cassette="main.yaml")  # explicit kwarg
def test_apis(my_cassette):  # fixture name matches the kwarg
    with my_cassette:  # explicit kwarg enforces context manager usage (just like in the above case with two of those)
        requests.get(...)
    my_cassette.rewind()  # possible given the cassette itself now
    with my_cassette:
        requests.post(...)
Diaoul commented 4 years ago

I like the idea of having more control over the cassettes being used.

Diaoul commented 4 years ago

So here is my use case as it is slightly different from what this ticket was originally about: I want to isolate some recordings done in a fixture in a specific cassette so it is not recorded for every test running the fixture.

Example: a fixture that provides a logged in client for a specific API to other tests

# this I want in a specific cassette
@pytest.fixture(scope="module")
async def client():
    async with SomeClient() as client:
        await client.login("username", "password")
        yield client

# this can be on it's own cassette, without the login part
@pytest.mark.vcr
@pytest.mark.asyncio
async def test_complex_operation(client):
    response = await client.complex_operation()

    assert response.status_code == 200
dogweather commented 10 months ago

How about marking a class? I just tested this and it works great:

@pytest.mark.default_cassette("crawl-nav.yaml")
class TestCrawlNavPages:
    @pytest.mark.vcr
    def test_number_of_results(_):
        pages = web_crawler.crawl_nav_pages(URL)
        assert len(pages) == 24

    @pytest.mark.vcr
    def test_returns_the_home_page(_):
        pages = web_crawler.crawl_nav_pages(URL)
        assert URL in [page.url for page in pages]

    @pytest.mark.vcr
    def test_crawl_data(_):
        ...