You can pass arguments to fixtures with the params keyword argument in the fixture decorator, and you can also pass arguments to tests with the @pytest. mark. parametrize decorator for individual tests.
pytest enables test parametrization at several levels: pytest. fixture() allows one to parametrize fixture functions.
usefixtures("fixture-name") . This special decorator adds the fixture to a test class, and the fixture will be executed before any test function.
This is actually supported natively in py.test via indirect parametrization.
In your case, you would have:
@pytest.fixture
def tester(request):
"""Create tester object"""
return MyTester(request.param)
class TestIt:
@pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
def test_tc1(self, tester):
tester.dothis()
assert 1
Update: Since this the accepted answer to this question and still gets upvoted sometimes, I should add an update. Although my original answer (below) was the only way to do this in older versions of pytest as others have noted pytest now supports indirect parametrization of fixtures. For example you can do something like this (via @imiric):
# test_parameterized_fixture.py
import pytest
class MyTester:
def __init__(self, x):
self.x = x
def dothis(self):
assert self.x
@pytest.fixture
def tester(request):
"""Create tester object"""
return MyTester(request.param)
class TestIt:
@pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
def test_tc1(self, tester):
tester.dothis()
assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items
test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED
However, although this form of indirect parametrization is explicit, as @Yukihiko Shinoda points out it now supports a form of implicit indirect parametrization (though I couldn't find any obvious reference to this in the official docs):
# test_parameterized_fixture2.py
import pytest
class MyTester:
def __init__(self, x):
self.x = x
def dothis(self):
assert self.x
@pytest.fixture
def tester(tester_arg):
"""Create tester object"""
return MyTester(tester_arg)
class TestIt:
@pytest.mark.parametrize('tester_arg', [True, False])
def test_tc1(self, tester):
tester.dothis()
assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items
test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED
I don't know exactly what are the semantics of this form, but it seems that pytest.mark.parametrize
recognizes that although the test_tc1
method does not take an argument named tester_arg
, the tester
fixture that it's using does, so it passes the parametrized argument on through the tester
fixture.
I had a similar problem--I have a fixture called test_package
, and I later wanted to be able to pass an optional argument to that fixture when running it in specific tests. For example:
@pytest.fixture()
def test_package(request, version='1.0'):
...
request.addfinalizer(fin)
...
return package
(It doesn't matter for these purposes what the fixture does or what type of object the returned package
) is.
It would then be desirable to somehow use this fixture in a test function in such a way that I can also specify the version
argument to that fixture to use with that test. This is currently not possible, though might make a nice feature.
In the meantime it was easy enough to make my fixture simply return a function that does all the work the fixture previously did, but allows me to specify the version
argument:
@pytest.fixture()
def test_package(request):
def make_test_package(version='1.0'):
...
request.addfinalizer(fin)
...
return test_package
return make_test_package
Now I can use this in my test function like:
def test_install_package(test_package):
package = test_package(version='1.1')
...
assert ...
and so on.
The OP's attempted solution was headed in the right direction, and as @hpk42's answer suggests, the MyTester.__init__
could just store off a reference to the request like:
class MyTester(object):
def __init__(self, request, arg=["var0", "var1"]):
self.request = request
self.arg = arg
# self.use_arg_to_init_logging_part()
def dothis(self):
print "this"
def dothat(self):
print "that"
Then use this to implement the fixture like:
@pytest.fixture()
def tester(request):
""" create tester object """
# how to use the list below for arg?
_tester = MyTester(request)
return _tester
If desired the MyTester
class could be restructured a bit so that its .args
attribute can be updated after it has been created, to tweak the behavior for individual tests.
I couldn't find any document, however, it seems to work in latest version of pytest.
@pytest.fixture
def tester(tester_arg):
"""Create tester object"""
return MyTester(tester_arg)
class TestIt:
@pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
def test_tc1(self, tester):
tester.dothis()
assert 1
You can access the requesting module/class/function from fixture functions (and thus from your Tester class), see interacting with requesting test context from a fixture function. So you could declare some parameters on a class or module and the tester fixture can pick it up.
To improve a little bit imiric's answer: Another elegant way to solve this problem is to create "parameter fixtures". I personally prefer it over the indirect
feature of pytest
. This feature is available from pytest_cases
, and the original idea was suggested by Sup3rGeo.
import pytest
from pytest_cases import param_fixture
# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)
@pytest.fixture
def tester(var):
"""Create tester object"""
return MyTester(var)
class TestIt:
def test_tc1(self, tester):
tester.dothis()
assert 1
Note that pytest-cases
also provides @fixture
that allow you to use parametrization marks directly on your fixtures instead of having to use @pytest.fixture(params=...)
from pytest_cases import fixture, parametrize
@fixture
@parametrize("var", [['var1', 'var2']], ids=str)
def tester(var):
"""Create tester object"""
return MyTester(var)
and @parametrize_with_cases
that allows you to source your parameters from "case functions" that may be grouped in a class or even a separate module. See doc for details. I'm the author by the way ;)
I made a funny decorator that allows writing fixtures like this:
@fixture_taking_arguments
def dog(request, /, name, age=69):
return f"{name} the dog aged {age}"
Here, to the left of /
you have other fixtures, and to the right you have parameters that are supplied using:
@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
assert dog == "Buddy the dog aged 7"
This works the same way function arguments work. If you don't supply the age
argument, the default one, 69
, is used instead. if you don't supply name
, or omit the dog.arguments
decorator, you get the regular TypeError: dog() missing 1 required positional argument: 'name'
. If you have another fixture that takes argument name
, it doesn't conflict with this one.
Async fixtures are also supported.
Additionally, this gives you a nice setup plan:
$ pytest test_dogs_and_owners.py --setup-plan
SETUP F dog['Buddy', age=7]
...
SETUP F dog['Champion']
SETUP F owner (fixtures used: dog)['John Travolta']
A full example:
from plugin import fixture_taking_arguments
@fixture_taking_arguments
def dog(request, /, name, age=69):
return f"{name} the dog aged {age}"
@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
yield f"{name}, owner of {dog}"
@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
assert dog == "Buddy the dog aged 7"
@dog.arguments("Champion")
class TestChampion:
def test_with_dog(self, dog):
assert dog == "Champion the dog aged 69"
def test_with_default_owner(self, owner, dog):
assert owner == "John Doe, owner of Champion the dog aged 69"
assert dog == "Champion the dog aged 69"
@owner.arguments("John Travolta")
def test_with_named_owner(self, owner):
assert owner == "John Travolta, owner of Champion the dog aged 69"
The code for the decorator:
import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain
_NOTHING = object()
def _omittable_parentheses_decorator(decorator):
@wraps(decorator)
def wrapper(*args, **kwargs):
if not kwargs and len(args) == 1 and callable(args[0]):
return decorator()(args[0])
else:
return decorator(*args, **kwargs)
return wrapper
@dataclass
class _ArgsKwargs:
args: ...
kwargs: ...
def __repr__(self):
return ", ".join(chain(
(repr(v) for v in self.args),
(f"{k}={v!r}" for k, v in self.kwargs.items())))
def _flatten_arguments(sig, args, kwargs):
assert len(sig.parameters) == len(args) + len(kwargs)
for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
yield arg if arg is not _NOTHING else kwargs[name]
def _get_actual_args_kwargs(sig, args, kwargs):
request = kwargs["request"]
try:
request_args, request_kwargs = request.param.args, request.param.kwargs
except AttributeError:
request_args, request_kwargs = (), {}
return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs
@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
def decorator(func):
original_signature = signature(func)
def new_parameters():
for param in original_signature.parameters.values():
if param.kind == Parameter.POSITIONAL_ONLY:
yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)
new_signature = original_signature.replace(parameters=list(new_parameters()))
if "request" not in new_signature.parameters:
raise AttributeError("Target function must have positional-only argument `request`")
is_async_generator = isasyncgenfunction(func)
is_async = is_async_generator or iscoroutinefunction(func)
is_generator = isgeneratorfunction(func)
if is_async:
@wraps(func)
async def wrapper(*args, **kwargs):
args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
if is_async_generator:
async for result in func(*args, **kwargs):
yield result
else:
yield await func(*args, **kwargs)
else:
@wraps(func)
def wrapper(*args, **kwargs):
args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
if is_generator:
yield from func(*args, **kwargs)
else:
yield func(*args, **kwargs)
wrapper.__signature__ = new_signature
fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)
def parametrizer(*args, **kwargs):
return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)
fixture.arguments = parametrizer
return fixture
return decorator
You can also use closures, which will give you more comprehensive naming and control on the params. They're more "explicit" than the request
param used in the implicit parametrization:
@pytest.fixture
def tester():
# Create a closure on the Tester object
def _tester(first_param, second_param):
# use the above params to mock and instantiate things
return MyTester(first_param, second_param)
# Pass this closure to the test
yield _tester
@pytest.mark.parametrize(['param_one', 'param_two'], [(1,2), (1000,2000)])
def test_tc1(tester, param_one, param_two):
# run the closure now with the desired params
my_tester = tester(param_one, param_two)
# assert code here
I use this to build configurable fixtures
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With