Skip to content

dynamically generated fixtures #2424

Closed
Closed

Description

@majuscule

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()

Activity

  1. added
    type: proposalproposal for a new feature, often to gather opinions or design the API around the new feature
    topic: fixturesanything involving fixtures directly or indirectly
    on Sep 28, 2017
  2. nolar commented on Oct 1, 2017

    @nolar

    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 is 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 generated 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.

  3. nicoddemus commented on Oct 1, 2017

    @nicoddemus
    Member

    @nolar that's a nice snippet, thanks!

  4. nicoddemus commented on Oct 1, 2017

    @nicoddemus
    Member

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

  5. nicoddemus commented on Mar 20, 2018

    @nicoddemus
    Member

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

  6. briancappello commented on Mar 25, 2018

    @briancappello

    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.

  7. nicoddemus commented on Mar 26, 2018

    @nicoddemus
    Member

    Thanks for sharing @briancappello!

  8. maxandersen commented on Dec 15, 2018

    @maxandersen

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

    with the above you get:

    my_user
        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 ?

  9. asottile commented on Jan 13, 2019

    @asottile
    Member

    @maxandersen #4636 (comment) has an example where docstrings work

  10. dan-sel commented on Apr 29, 2021

    @dan-sel

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

    @nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

  11. nicoddemus commented on Apr 29, 2021

    @nicoddemus
    Member

    @nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

    Sorry which API?

  12. asottile commented on Apr 29, 2021

    @asottile
    Member

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

    @nicoddemus Can we consider this API then? I'm hesitant to modify globals() unless I can be sure this behaviour of pytest won't change in the near future.

    from a python standpoint (and therefore pytest) there's not really a noticeable difference between:

    x = 1

    and

    def f(name, value):
        globals()[name] = value
    
    f('x', 1)
  13. dan-sel commented on Apr 29, 2021

    @dan-sel

    Sorry, bad hand-mind coordination.
    What I actually wanted to know is whether we can rely on the workaround to work in the future. It seems to be based on an undocumented implementation detail ("pytest scans for the fixture by iterating over dir(holderobj)"). If that is subject to change anytime without prior notice, we (and probably most others) will not be able to use it. And in that case, it would be great to have it as an official feature.

  14. nicoddemus commented on Apr 30, 2021

    @nicoddemus
    Member

    Hmm it is hard to make that promise, but we can say with confidence that there are no plans at the moment to change that, even in the far future.

    If that were to change however, we probably will give this prior notice as we understand this might break existing code.

  15. GrayedFox commented on Feb 14, 2022

    @GrayedFox

    I deleted my previous comment since it assumed a problem with parametrization when really the issue is about importing helper methods that generate parametrized fixtures. I've opened a stackoverflow question about it here.

    I can confirm that this trick still works for class scoped parametrized fixtures 👍🏾

  16. GrayedFox commented on Feb 16, 2022

    @GrayedFox

    Hey just leaving this here in case people find it useful - the reason this trick didn't work when importing the methods from a separate helper file is that helper file has a different global scope, and the hack works by changing the global fixture scope at run time. Here's a working example that generates a parametrized fixture that also uses the bundled request fixture for class introspection:

    import inspect
    import pytest
    
    def generate_fixture(scope, params):
        @pytest.fixture(scope=scope, params=params)
        def my_fixture(request):
            request.cls.param = request.param
            print(request.param)
    
        return my_fixture
    
    def inject_fixture(name, scope, params):
        """Dynamically inject a fixture at runtime"""
        # we need the caller's global scope for this hack to work hence the use of the inspect module
        caller_globals = inspect.stack()[1][0].f_globals
        # for an explanation of this trick and why it works go here: https://github.com/pytest-dev/pytest/issues/2424
        caller_globals[name] = generate_fixture(params)
  17. moshesar commented on Apr 7, 2022

    @moshesar

    @nolar that's very helpful thank you! Can you suggest a wider approach where multiple test generate the same fixture, but with different params ? I think using globals in this case might cause error where 2 tests can reference to the same fixture name but get unexpected data.

  18. patha454 commented on Jan 31, 2024

    @patha454

    Is it possible to generalise the snippets here to be injected from a different module than the tests?

    I'm writing a PyTest plugin for backwards compatibility with a legacy, in-house testing framework. I'm attempting to implement a plugin which creates a wrapper fixture, and attaches it to all relevant tests.

    The adapted code fail with the fixture not being found, despite being present in the test module's globals()

    def pytest_collection_modifyitems(session, config, items: List[Item]):
        for item in items:
            pytest_function: Function = item.getparent(Function)
            def generate_fixture(name: str):
                @pytest.fixture(name=name, scope="module")
                def _fixture():
                    print("Setup...")
                    yield
                    print("Teardown...")
                return _fixture
    
            def inject_fixture(name: str, module: ModuleType):
                setattr(module, name, generate_fixture(name))
    
            module: ModuleType = pytest_function.module
            inject_fixture("example_fixture", module)
            pytest_function.fixturenames.append("example_fixture")
      def test_dummy_func():
    E       fixture 'example_fixture' not found
    >       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
    >       use 'pytest --fixtures [testpath]' for help on them.
    # Printing globals from withing a test
    ------------------- Captured stdout call --------------------
    ... 'example_fixture': <function pytest_collection_modifyitems.<locals>.generate_fixture.<locals>._fixture at 0x7f97ca10e1f0>
    ...
    

    I'm aware of the pytest_runtest_teardown and pytest_runtest_setup PyTest hooks for running legacy function scope hooks - but I also need support for legacy module scope hooks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: fixturesanything involving fixtures directly or indirectlytype: proposalproposal for a new feature, often to gather opinions or design the API around the new feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions