Search code examples
pythoncommand-line-argumentspytestfixturesparameterization

Cleaner way to do pytest fixture parameterization based on command-line switch?


I've technically already solved the problem I was working on, but I can't help but feel like my solution is ugly:

I've got a pytest suite that I can run in two modes: Local Mode (for developing tests; everything just runs on my dev box through Chrome), and Seriousface Regression Testing Mode (for CI; the suite gets run on a zillion browsers and OSes). I've got a command-line flag to toggle between the two modes, --test-local. If it's there, I run in local mode. If it's not there, I run in seriousface mode. Here's how I do it:

# contents of conftest.py
import pytest

def pytest_addoption(parser):
    parser.addoption("--test-local", action="store_true", default=False, help="run locally instead of in seriousface mode")

def pytest_generate_tests(metafunc):
    if "dummy" in metafunc.fixturenames:
        if metafunc.config.getoption("--test-local"):
            driverParams = [(True, None)]
        else:
            driverParams = [(False, "seriousface setting 1"), (False, "seriousface setting 2")]
        metafunc.parameterize("dummy", driverParams)

@pytest.fixture(scope="function")
def driver(dummy):
    _driver = makeDriverStuff(dummy[0], dummy[1])
    yield _driver
    _driver.cleanup()

@pytest.fixture
def dummy():
    pass

The problem is, that dummy fixture is hideous. I've tried having pytest_generate_tests parameterize the driver fixture directly, but it ends up replacing the fixture rather than just feeding stuff into it, so cleanup() never gets called when the test finishes. Using the dummy lets me replace the dummy with my parameter tuple, so that that gets passed into driver().

But, to reiterate, what I have does work, it just feels like a janky hack.


Solution

  • You can try a different approach: instead of dynamically selecting the parameter set for the test, declare ALL parameter sets on it, but deselect the irrelevant ones at launch time.

    # r.py
    import pytest
    
    real = pytest.mark.real
    mock = pytest.mark.mock
    
    @pytest.mark.parametrize('a, b', [
        real((True, 'serious 1')),
        real((True, 'serious 2')),
        mock((False, 'fancy mock')),
        (None, 'always unmarked!'),
    ])
    def test_me(a, b):
        print([a, b])
    

    Then run it as follows:

    pytest -ra -v -s   r.py -m real        # strictly marked sets
    pytest -ra -v -s   r.py -m mock        # strictly marked sets
    pytest -ra -v -s   r.py -m 'not real'  # incl. non-marked sets
    pytest -ra -v -s   r.py -m 'not mock'  # incl. non-marked sets
    

    Also, skipif marks can be used for the selected parameter set (different from deselecting). They are natively supported by pytest. But the syntax is quite ugly. See more in the pytest's Skip/xfail with parametrize section.

    The official manual also contains exactly the same case as in your question in Custom marker and command line option to control test runs. However, it is also not as elegant as -m test deselection, and is more suitable for complex run-time conditions, not on the apriori known structure of the tests.