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?

Jason Antman
  • 2,620
  • 2
  • 24
  • 26
  • 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. – benjimin Sep 07 '21 at 22:20

5 Answers5

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

Ilya Karpeev
  • 1,033
  • 8
  • 3
  • 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. – Jason Antman Jan 30 '15 at 23:10
  • 2
    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 fits – Ilya Karpeev Feb 03 '15 at 15:02
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
Jason Antman
  • 2,620
  • 2
  • 24
  • 26
  • 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. – benjimin Sep 07 '21 at 22:16
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.

John Mee
  • 50,179
  • 34
  • 152
  • 186
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):
Carlos P Ceballos
  • 384
  • 1
  • 7
  • 20
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).