Any way to pass parameters into pytest fixture?
Asked Answered
A

5

11

I am not talking about the Parameterizing a fixture feature that allows a fixture to be run multiple times for a hard-coded set of parameters.

I have a LOT of tests that follow a pattern like:

httpcode = 401  # this is different per call
message = 'some message'  # this is different per call
url = 'some url'  # this is different per call


mock_req = mock.MagicMock(spec_set=urllib2.Request)
with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
     mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
    mock_request.return_value = mock_req
    mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
    connection = MyClass()
    with pytest.raises(MyException):
        connection.some_function()  # this changes

Essentially, I have a class that's an API client, and includes custom, meaningful exceptions that wrap urllib2 errors in something API-specific. So, I have this one pattern - patching some methods, and setting side effects on one of them. I use it in probably a dozen different tests, and the only differences are the three variables which are used in part of the side_effect, and the method of MyClass() that I call.

Is there any way to make this a pytest fixture and pass in these variables?

Anoa answered 19/1, 2015 at 14:53 Comment(1)
If the core code only differed between tests by the method name, you could have just one test (using getattr) and pass the method name (and perhaps a keyword dict of any call arguments, plus your custom exception type) as additional components in your parameter set.Degree
I
19

You can use indirect fixture parametrization http://pytest.org/latest/example/parametrize.html#deferring-the-setup-of-parametrized-resources

@pytest.fixture()
def your_fixture(request):
    httpcode, message, url = request.param
    mock_req = mock.MagicMock(spec_set=urllib2.Request)
    with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
         mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
        mock_request.return_value = mock_req
        mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
        connection = MyClass()
        with pytest.raises(MyException):
            connection.some_function()  # this changes


@pytest.mark.parametrize('your_fixture', [
    (403, 'some message', 'some url')
], indirect=True)
def test(your_fixture):
   ...

and your_fixture will run before test with desired params

Inglenook answered 20/1, 2015 at 18:37 Comment(2)
I specifically said this isn't what I want... I don't want the fixture to be created multiple times, I just want to pass parameters into it.Anoa
In my code fixture runs only once in test. In fact my code does the same as yours. The difference is in way in which parameters are passed. If you want to generate parameters inside test and then pass them to fixture - your code is only way to do it. If parameters are predefined in test, then my code also fitsInglenook
A
6

I've done a bunch more research on this since posting my question, and the best I can come up with is:

Fixtures don't work this way. Just use a regular function, i.e.:

def my_fixture(httpcode, message, url):
    mock_req = mock.MagicMock(spec_set=urllib2.Request)
    with mock.patch('package.module.urllib2.urlopen', autospec=True) as mock_urlopen, \
         mock.patch('package.module.urllib2.Request', autospec=True) as mock_request:
        mock_request.return_value = mock_req
        mock_urlopen.side_effect = urllib2.HTTPError(url, httpcode, message, {}, None)
        connection = MyClass()
        return (connection, mock_request, mock_urlopen)

def test_something():
    connection, mock_req, mock_urlopen = my_fixture(401, 'some message', 'some url')
    with pytest.raises(MyException):
        connection.some_function()  # this changes
Anoa answered 30/1, 2015 at 23:11 Comment(1)
I would implement your "fixture" using @contextlib.contextmanager (and yield rather than return, etc) otherwise python will attempt to teardown/unpatch your environment (here executing mock's __exit__ methods) before passing back into your test.Degree
J
2

How to pass parameters into a fixture?

Unpack that idea for a moment: you're asking for a fixture, which is a function, which reacts to parameters. So, return a function, which reacts to parameters:

@pytest.fixture
def get_named_service():
    def _get_named_service(name):
        result = do_something_with_name(name)
        return result
    return _get_named_service

Thus, in the test, you can provide the parameters to the function:

def test_stuff(get_named_service):
    awesome_service = get_named_service('awesome')
    terrible_service = get_named_service('terrible')
    # Now you can do stuff with both services.

This is documented as a factory pattern:
https://docs.pytest.org/en/latest/how-to/fixtures.html#factories-as-fixtures

Which, as the OP found, is just a function, but with the advantage of being inside the conftest where all the other common utils and setup/teardown code resides; plus self-documenting the dependencies of the test.

Jalisajalisco answered 29/4, 2020 at 1:42 Comment(0)
E
0

I know this is old, but maybe it helps someone who stumbles on this again

@pytest.fixture
def data_patcher(request):

    def get_output_test_data(filename, as_of_date=None):
         # a bunch of stuff to configure output
        return output

    def teardown():
        pass

    request.addfinalizer(teardown)

    return get_output_test_data

and then, inside the function:

with patch('function to patch', new=data_patcher):
East answered 17/1, 2018 at 17:53 Comment(0)
A
0

Some trick with pytest.mark and we have a fixture with arguments.

from allure import attach
from pytest import fixture, mark


def get_precondition_params(request_fixture, fixture_function_name: str):
    precondition_params = request_fixture.keywords.get("preconditions_params")
    result = precondition_params.args[0].pop(fixture_function_name) if precondition_params is not None else None
    return result


@fixture(scope="function")
def setup_fixture_1(request):
    params = get_precondition_params(request, "setup_fixture_1")
    return params


@mark.preconditions_params(
    {
        "setup_fixture_1": {
            "param_1": "param_1 value for setup_fixture_1",
            "param_2": "param_2 value for setup_fixture_1"
        },
    }
)
def test_function(setup_fixture_1):
    attach(str(setup_fixture_1), "setup_fixture_1 value")

Now we can use one fixture code, parametrize it with mark, and do anything with params inside fixture. And fixture will be executed as precondition (how it must be), not as step (like it will be if we return function from fixture).

Adnate answered 19/5, 2022 at 14:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.