-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Allow list of dictionaries for @pytest.mark.parametrize #7568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I'd love to have something like this, but the problem is that there isn't really an agreement what dictionaries mean exactly when passing them to I was wondering whether it'd make more sense for ((datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), # already works
pytest.param(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), # already works
pytest.param(a=datetime(2001, 12, 12), b=datetime(2001, 12, 11), expected=timedelta(1)), # new and seems logical But the problem with that is that
|
@The-Compiler thanks for a quick reply. As I understood, previous discussion (#5487 and #5850) are mostly about test cases' descriptions/ids, which are also required to improve the developer experience (DX). I will add more explanation to what I have written above. Probably, the most frustrating issue at the moment is that the developer should support a list of arguments that duplicates already declared function/method parameters method next to it: @pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected): It would be a great improvement to avoid writing this by extracting meta-information from the function/method signature. IMO it is worth to pay a small amount of performance and include some "magic" for DX enhancement. @pytest.mark.parametrize(testdata)
def test_timedistance_v0(a, b, expected): The proposed approach (allow maintain parameters names in test data as a dictionary) gives a possibility to have readable test data. It would be great to enrich it with test case id as well. So, in the end, we can come up with the following structure: testdata = {
'test case 1': {
'a': datetime(2001, 12, 12),
'b': datetime(2001, 12, 11),
'expected': timedelta(1),
},
'test case 2': {
'a': datetime(2001, 12, 11),
'b': datetime(2001, 12, 12),
'expected': timedelta(-1),
},
} Talking about Probably, it is possible to utilize
|
Sometimes, explicit is better than implicit 😉 Arguments can also refer to fixtures, so having an explicit list of arguments to parametrize helps to get e.g. sensible error messages when there is a typo. I also don't think it makes sense to have yet another "magic" data structure API for how to use parametrize, there are already various things in there (specifying a list of items for a single argument vs. a list of tuples for multiple, etc. etc.). This gets confusing fast. Case in point: In your example above, you'd now always have to explicitly specify test IDs, which can get cumbersome fast. Perhaps you want something like pytest-cases though? So, I'd still like to explore a more explicit solution involving |
I actually do this: import datetime as dt
import attr
import pytest
@attr.dataclass(fronzen=True)
class TimeDistanceParam:
a: dt.datetime
b: dt.datetime
expected: dt.timedelta
def pytest_id(self):
return repr(self) # usually something custom
@pytest.mark.parametrize(
"p",
[
TimeDistanceParam(
a=dt.datetime(2001, 12, 12),
b=dt.datetime(2001, 12, 11),
expected=dt.timedelta(1),
),
TimeDistanceParam(
a=dt.datetime(2001, 12, 11),
b=dt.datetime(2001, 12, 12),
expected=dt.timedelta(-1),
),
],
ids=TimeDistanceParam.pytest_id,
)
def test_timedistance_v0(p):
assert p.a - p.b == p.expected |
something that would help my usacase is a PytestParam abc so I can do: class PytestParam(metaclass=abc.ABCMeta):
@abc.abstractmethod
def pytest_marks(self): ...
@abc.abstractmethod
def pytest_id(self): ... @pytest.PytestParam.register
@attr.dataclass(frozen=True)
class TimeDistanceParam:
a: dt.datetime
b: dt.datetime
def pytest_marks():
...
def pytest_id():
... and pytest.mark.parametrize can special case registered instances of |
fwiw, I'm still -1 on this as per the previous (duplicate) discussions. |
I'm -1 on this as well, as it seems fairly easy to do in user code: import datetime as dt
import pytest
def params(d):
return pytest.mark.parametrize(
argnames=(argnames := sorted({k for v in d.values() for k in v.keys()})),
argvalues=[[v.get(k) for k in argnames] for v in d.values()],
ids=d.keys(),
)
@params(
{
"test case 1": {
"a": dt.datetime(2001, 12, 12),
"b": dt.datetime(2001, 12, 11),
"expected": dt.timedelta(1),
},
"test case 2": {
"a": dt.datetime(2001, 12, 11),
"b": dt.datetime(2001, 12, 12),
"expected": dt.timedelta(-1),
},
}
)
def test_timedistance_v0(a, b, expected):
assert a - b == expected |
In this case, it is too explicit because as I already said: "...developer should support a list of arguments that duplicates already declared function/method parameters method next to it..."
Unfortunately, I multiple times forgot to add parameters into this list and got an error that does not have relation to broken code or incorrect test logic. So I can conclude that the duplicated list of parameters does not help in most cases.
The proposed data structure is not an alternative to already existing. It is a logical continuation list of tuples for cases when a developer wants to explicitly provide parameters names and/or ids for test cases. So we are not introducing invariant of usage.
Probably, I didn't mention but I would like to keep BC with the old structure. So the developers would not be required to provide IDs if they don't want to. FindingsAfter some investigation of why parameters list is required, I found the place where the real magic lives. It is possible to stack decorators to get all possible combinations: @pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass In this case list of parameters is required to avoid invariant of list usage. It is impossible to say is it list parameters or list of test cases w/o explicit list of parameters. IMO it is unfounded API complication that can be replaced with more readable variant using @pytest.mark.parametrize("x,y", itertools.product([0,1], [2,3]))
def test_foo(x, y):
pass or single parameter parameterization should live in separate decorator. ConclusionParametrize API already overcomplicated. |
Stacked decorator are not the only source of parameterization, any fixture as well as custom plugins can participate It's a well used feature so your proposal doesn't exactly help/work for the project. |
I appreciate everyone’s input into this ticket’s discussion. I'm closing this ticket because it is discussed. |
I was directed to here from #7790 since I have a similar problem with readability and maintainability when using (I hope it's okay to post here even though the thread is closed). I use a helper function that wraps the
It's used like this:
Gives:
The advantages of this over plain
The advantages over other suggestions in this thread are:
Disadvantages:
I think something like this would be a nice addition to pytest. EDIT Actually I overlooked a simpler way: we can use the positional-only argument feature of Python 3.8 to remove the need for the
That means we can just pass in an optional string at the beginning of the
|
Make sure you check out |
For anyone still looking for a simple solution, here is mine. In the top level def dict_parametrize(data, **kwargs):
args = list(list(data.values())[0].keys())
formatted_data = [[item[a] for a in args] for item in data.values()]
ids = list(data.keys())
return pytest.mark.parametrize(args, formatted_data, ids=ids, **kwargs) And then in your tests you can use it like so: from conftest import dict_parametrize
@dict_parametrize(
{
"these keys become the ids that identify each run": {
"x": 1,
"y": 2,
"expected_value": 3,
},
"some_edge_case": {
"x": 0,
"y": 0,
"expected_value": 0,
},
}
)
def test_some_func(x, y, expected_value):
# Just ensure you declare the first dictionary args
# in the same order as they're passed in.
assert x + y == expected_value The caveat for this very simple implementation is that the dictionary in the first scenarios needs to match the order of args. But the readability increase for larger test suites, especially linking the specific parameterization to a given id, is huge. |
I also made a small package that does this, https://github.com/ckp95/pytest-parametrize-cases |
Pytest-paraterisarion does the job as well. https://pypi.org/project/pytest-parametrization/ Would be good to have this syntax by default, this helps maintaining long list of test cases (having the name of the test case far from the parameter is really a bad design choice from pytest.parametrize |
|
One way to inject readable parameter names close to their values is to define a NamedTuple and then unpack it into the import typing
import numpy
import pytest
class Param(typing.NamedTuple):
y: numpy.typing.ArrayLike
expected: dict[str, float]
@pytest.mark.slow()
@pytest.mark.parametrize(
("y", "expected"),
[
pytest.param(
*Param(y=np.array([...), expected={ T_s": 95.3, ... }),
id="low",
)
],
)
def test_model_fit(..., y, expected):
"""Test that the model fit is as expected."""
... |
Just throwing in here that reading this discussion I realized one can also just do: @pytest.mark.parametrize(
"p",
[
{
"a": dt.datetime(2001, 12, 12),
"b": dt.datetime(2001, 12, 11),
"expected": dt.timedelta(1),
},
{
"a": dt.datetime(2001, 12, 11),
"b": dt.datetime(2001, 12, 12),
"expected": dt.timedelta(-1),
},
],
)
def test_timedistance_v0(p):
assert p["a"] - p["b"] == p["expected"] ... or some variation thereof. I actually tend to like to do the following, that works well also for multiple return values: @pytest.mark.parametrize(
"p",
[
{
"have": {
"a": dt.datetime(2001, 12, 12),
"b": dt.datetime(2001, 12, 11),
},
"want": {
"result": dt.timedelta(1),
},
},
{
"have": {
"a": dt.datetime(2001, 12, 11),
"b": dt.datetime(2001, 12, 12),
},
"want": {
"result": dt.timedelta(-1),
},
},
],
)
def test_timedistance_v0(p):
assert p["have"]["a"] - p["have"]["b"] == p["want"]["result"] Hadn't thought about that before, so thank you! :) |
Right now it is only allowed list of tuples to pass into
@pytest.mark.parametrize
decorator:The large test data is complicated to manage
testdata
and it became less readable. To overcome this issue I proposing passtestdata
as a list of dictionaries and keep arguments names only intestdata
:The text was updated successfully, but these errors were encountered: