How do I use a different fixture implementation based on a command line argument?

Viewed 38

I am testing an app that has user profiles. Normally, I tear down the profile after each test, but it is very slow, so I wanted to have the option to run the test faster via keeping the profile but tearing down changes after each test.

This is what I have now, and it works fine:

@pytest.fixture(scope="session")
def session_scope_app():
    with empty_app_started() as app:
        yield app


@pytest.fixture(scope="session")
def session_scope_app_with_profile_loaded(session_scope_app):
    with profile_loaded(session_scope_app):
        yield session_scope_app


if TEAR_DOWN_PROFILE_AFTER_EACH_TEST:
    @pytest.fixture
    def setup(session_scope_app):
        with profile_loaded(session_scope_app):
            yield session_scope_app
else:
    @pytest.fixture
    def setup(session_scope_app_with_profile_loaded):
        with profile_state_preserved(session_scope_app_with_profile_loaded):
            yield session_scope_app_with_profile_loaded

This produces a fixture setup that, as far as other tests are concerned, behaves the same way regardless of whether the profile is torn down after each test.

Now, I want to turn TEAR_DOWN_PROFILE_AFTER_EACH_TEST into a command line option. How can I do this? Command line options are not yet available in test collection stage, and I can't just put the if into the fixture function body, as the two variants of setup depend on different fixtures.

1 Answers

There are two ways of doing that, but first, let's add the command option itself.

def pytest_addoption(parser):
    parser.addoption("--tear-down-profile-after-each-test",
                     action="store_true",
                     default=True)
    parser.addoption("--no-tear-down-profile-after-each-test", "-T",
                     action="store_false",
                     dest="tear_down_profile_after_each_test")

Now, we can either invoke fixtures dynamically, or create a tiny plugin that shuffles our fixtures.

Invoke the fixture dynamically

This is very simple. Instead of depending on a fixture via function arguments, we can call request.getfixturevalue(name) from inside the fixture.

@pytest.fixture
def setup(session_scope_app):
    if request.config.option.tear_down_profile_after_each_test:
        with profile_loaded(session_scope_app):
            yield session_scope_app
    else:
        session = request.getfixturevalue(
            session_scope_app_with_profile_loaded.__name__
        )
        with profile_state_preserved(session):
            yield session

(It's ok to depend on session_scope_app since session_scope_app_with_profile_loaded depends on it anyway.)

Pros: PyCharm is happy. Cons: you won't be seeing session_scope_app_with_profile_loaded in --setup-plan.

Make a simple plugin

Plugins have the benefit of having access to the configuration.

def pytest_configure(config):
    class Plugin:
        if config.option.tear_down_profile_after_each_test:
            @pytest.fixture
            def setup(self, session_scope_app):
                with profile_loaded(session_scope_app):
                    yield session_scope_app
        else:
            @pytest.fixture
            def setup(self, session_scope_app_with_profile_loaded):
                with profile_state_preserved(session_scope_app_with_profile_loaded):
                    yield session_scope_app_with_profile_loaded

    config.pluginmanager.register(Plugin())

Pros: You get excellent --setup-plan. Cons: PyCharm won't recongize that setup is a fixture.

Related