Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pytest: how to use a mark to inject a fixture?

I'm writing a pytest plugin with a fixture that has a side effect of setting up some desirable mocks. I'd like to write a simple mark that will allow the user to call this fixture setup before the test runs, without having to include the fixture in the test function parameters -- essentially, "injecting" the fixture using a mark. My reasoning is that the user may want the mocked setup without needing the return value of the fixture itself, in which case it seems more intuitive to me to use a mark than to require that they declare a fixture they won't be using.

How might I use a mark to require a fixture in pytest? Looking at the docs, it seems like I want to hook into something like pytest_collection_modifyitems, check for the relevant mark on each item using Item.iter_markers, and then somehow update the list of fixtures. Reading the code, however, I'm having trouble figuring out how exactly to trigger that fixture setup.

Here's a simplified example of what the fixture in question looks like:

@pytest.fixture
def mocks(mocker):
    ret_val = 10
    mocker.patch('path.to.patch', return_value=ret_val)
    return ret_val

Here's what the user can do to set up the mocks now:

def test_mocks(mocks):
    # 'path.to.patch' will be mocked in this test
    # ... test code ...

But here's what the test might look like if the fixture could be triggered via a mark instead:

@pytest.mark.mocks
def test_mocks():
    # 'path.to.patch' will be mocked in this test, too
    # ... test code ...
like image 827
jeancochrane Avatar asked May 29 '18 21:05

jeancochrane


2 Answers

Use the usefixtures marker:

# conftest.py
import pytest

@pytest.fixture
def mocks(mocker):
    mocker.patch('os.path.isdir', return_value=True)

# test_me.py
import os
import pytest

@pytest.mark.usefixtures('mocks')
def test_mocks():
    assert os.path.isdir('/this/is/definitely/not/a/dir')

Passing multiple fixtures is also posible:

@pytest.mark.usefixtures('mocks', 'my_other_fixture', 'etc')

However, there is a caveat: the mocks fixture in your code returns a value ret_val. When passing the fixture via test function args, this value is returned under the mocks arg; when you use markers, you don't have the arg anymore, so you won't be able to use the value. Should you be needing the mock value, pass the fixture in the test function args. There are some other ways imaginable, like passing ret_val via cache, however, the resulting code will be more complicated and less readable so I wouldn't bother.

like image 57
hoefling Avatar answered Oct 20 '22 07:10

hoefling


I was able to implement this by using the pytest_collection_modifyitems hook to adjust the list of fixtures on the test item after collection:

@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    '''
    Check if any tests are marked to use the mock.
    '''
    for item in items:
        # Note that `get_marker` has been superceded by `get_closest_marker`
        # in the development version of pytest
        if item.get_marker('mocks'):
            item.fixturenames.append('mocks')

Adjusting the Item.fixturenames list after collection appears to trigger the fixture setup in the way I was hoping.

If you don't care about using a custom mark, however, @hoefling's recommendation to use the built-in usefixtures mark would be a good solution, too.

like image 20
jeancochrane Avatar answered Oct 20 '22 08:10

jeancochrane