Pytest: dynamically generated fixtures

Created on 22 May 2017  Â·  8Comments  Â·  Source: pytest-dev/pytest

This is a feature request. I would like to be able to dynamically generate fixtures, similar to parameterized fixtures, but instead of running all associated tests with each fixture, expose each fixture dynamically. The syntax could use work, but i.e.:

@pytest.fixture(scope='module',
                generate=['user;, 'admin'],
                params=[User, Admin])
def session(SessionType):
    return SessionType()

def test_authorization(session_user, session_admin):
    with pytest.raises(Exception):
        session_user.do_admin_thing()
     assert session_admin.do_admin_thing()

fixtures proposal

Most helpful comment

This is a rare and too specific use-case to make it into the framework. And it can be achieved easily right now. Here is the code snippet that works as you described:

import pytest

def generate_fixture(someparam):
    @pytest.fixture(scope='module')
    def my_fixture():
        print('my fixture called with someparam={!r}'.format(someparam))
        return someparam
    return my_fixture

def inject_fixture(name, someparam):
    globals()[name] = generate_fixture(someparam)

inject_fixture('my_user', 100)
inject_fixture('my_admin', 200)

def test_me(my_user, my_admin):
    print(my_user, my_admin)

The trick is that the module must contain a module-level variable (in the module's globals) with the name of the generate fixture — because pytest scans for the fixture by iterating over dir(holderobj) where holderobj is the module, not by explicitly registering a function on each call to pytest.fuxture(). So, you can have as many dynamically created fixtures with dynamically created names as you wish.

All 8 comments

This is a rare and too specific use-case to make it into the framework. And it can be achieved easily right now. Here is the code snippet that works as you described:

import pytest

def generate_fixture(someparam):
    @pytest.fixture(scope='module')
    def my_fixture():
        print('my fixture called with someparam={!r}'.format(someparam))
        return someparam
    return my_fixture

def inject_fixture(name, someparam):
    globals()[name] = generate_fixture(someparam)

inject_fixture('my_user', 100)
inject_fixture('my_admin', 200)

def test_me(my_user, my_admin):
    print(my_user, my_admin)

The trick is that the module must contain a module-level variable (in the module's globals) with the name of the generate fixture — because pytest scans for the fixture by iterating over dir(holderobj) where holderobj is the module, not by explicitly registering a function on each call to pytest.fuxture(). So, you can have as many dynamically created fixtures with dynamically created names as you wish.

@nolar that's a nice snippet, thanks!

@nolar you might consider contribute a post to pytest-tricks with that trick btw 😉

Closing as it seems the workaround is good enough to solve the original case.

I needed something that could inject fixtures based on the result of another fixture, for which the solution proposed above by @nolar does not work. (My specific use case is a Flask app with its own dependency injection system bolted on top, for which I wanted to be able to inject those services as fixtures)

For anybody else that needs something similar, this is what I came up:

@pytest.fixture(autouse=True)
def inject_services(app, request):
    # get the current fixture or test-function
    item = request._pyfuncitem
    # preemptively inject any arg_names that pytest doesn't know about, but that we do
    fixture_names = getattr(item, "fixturenames", request.fixturenames)
    for arg_name in fixture_names:
        if arg_name not in item.funcargs:
            try:
                request.getfixturevalue(arg_name)
            except FixtureLookupError:
                if arg_name in app.services:
                    item.funcargs[arg_name] = app.services[arg_name]

Seems to work fine, but no guarantees. Logic is based on pytest's FixtureRequest._fillfixtures method.

Thanks for sharing @briancappello!

anyone found a good way to have the dynamic fixtures have generated doc strings too ?

with the above you get:

    tests/conftest.py:11: no docstring available
my_admin
    tests/conftest.py:11: no docstring available

would be nice to also somehow control the documentation.

Anyone ?

@maxandersen https://github.com/pytest-dev/pytest/issues/4636#issuecomment-453870555 has an example where docstrings work

Was this page helpful?
0 / 5 - 0 ratings