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