Search code examples
pythonpytestdryfixtures

How can I use pytest options as fixture and not repeat myself?


I have a test suite with a conftest.py defining some options and some fixtures to retrieve them:

def pytest_addoption(parser):
  parser.addoption("--ip", action="store")
  parser.addoption("--port", action="store")

@pytest.fixture
def ip(request):
  return request.config.getoption("ip")

@pytest.fixture
def port(request):
  return request.config.getoption("ip")

(I slipped in a copy-paste error to make a point)

My tests can very eloquently express the options they need:

def test_can_ping(ip):
  ...

def test_can_net_cat(ip, port):
  ...

But ...

I'm trying to avoid duplicating myself here: I have to specify the name of the config parameter in three places to make it work.

I could have avoided the copy-paste error if I had something that looked like this:

# does not exist:
@pytest.option_fixture
def ip(request, parser):
  return request.config.getoption(this_function_name)

or this

def pytest_addoption(parser):
  # does not exist: an as_fixture parameter
  parser.addoption("--ip", action="store", as_fixture=True)
  parser.addoption("--port", action="store", as_fixture=True)

Is there a way to tell pytest to add an option and a corresponding fixture to achieve DRY/SPOT code?


Solution

  • After some tests, I came to something working. It is probably not the best way to do it but it is quite satisfying I think. All code below have been added to the conftest.py module, except the two tests.

    First define a dictionary containing the options data:

    options = {
        'port': {'action': 'store', 'help': 'TCP port', 'type': int},
        'ip': {'action': 'store', 'help': 'IP address', 'type': str},
    }
    

    We could do without help and type, but it will have a certain utility later.

    Then you can use this options to create the pytest options:

    def pytest_addoption(parser):
        for option, config in options.items():
            parser.addoption(f'--{option}', **config)
    

    At this point, pytest --help gives this (note the help data usage which provides convenient doc):

    usage: pytest [options] [file_or_dir] [file_or_dir] [...]
    ...
    custom options:
      --port=PORT           TCP port
      --ip=IP               IP address
    

    Finally we have to define the fixtures. I did this by providing a make_fixture function which is used in a loop at conftest.py reading to dynamically create fixtures and add them to the global scope of the module:

    def make_fixture(option, config):
        func = lambda request: request.config.getoption(option)
        func.__doc__ = config['help']
        globals()[option] = pytest.fixture()(func)
    
    
    for option, config in options.items():
        make_fixture(option, config)
    

    Again, the 'help' data is used to build a docstring to the created fixtures and document them. Thus, invoking pytest --fixtures prints this:

    ...
    ---- fixtures defined from conftest ----
    ip
        IP address
    port
        TCP port
    

    Invoking pytest --port 80 --ip 127.0.0.1, with the two following very simple tests, seems to validate the trick (Here the type data shows its utility, it has made pytest convert the port to an int, instead of a string):

    def test_ip(ip):
        assert ip == '127.0.0.1'
    
    
    def test_ip_port(ip, port):
        assert ip == '127.0.0.1'
        assert port == 80
    

    (Very interesting question, I would like to see more like this one)