-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Keyword-based parameter sets #9216
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 believe you just described namedtupe/dataclasses |
@RonnyPfannschmidt I'm essentially using dataclasses (the attrs variant of). The only interesting parts are:
As you can see, I already have a pattern in practice. What I'm wondering is whether I can put an end to copy-pasting this "paramset.py" between my projects, by instead getting If we don't want to enable derived types, then perhaps:
If we do, then perhaps:
I don't want to be too prescriptive, so I'm trying to gauge first if it sounds like a worthwhile change in general. |
I would like to see more details on your actual use case My initial instinct is to provide something like def as_param(self, id=None, marks=None):
return pytest.param(self, marks=marks, id=id) But that may very well miss the mark for your particular use case. However its critical to make a good initial choice on where the complexity will be as the down payment will be a drag otherwise. |
Personally I'd love to have some kind of syntax to pass keyword arguments into parametrize, without having to declare a dataclass for every test. However, using I have some more thoughts in a comment on an earlier related issue (which was closed by the author): #7568 (comment) |
Another possible syntax that I don't think I saw mentioned is:
This would require that no parameters be specified in the constructor, but doesn't limit either the arguments to |
That's a great point. Indeed pytest.param(foo=..., bar=..., id="what I'm testing").feature_i_cannot_foresee(42).another_feature('foobar') |
I think my proposal was conflating a few things:
Most of us can agree on the utility of (1) in complex tests. The motivation stems from: The second reason has also been the motivation for (2) and (2.1). In complex tests with dozens of arguments, it's hard to tell whether an argument is provided by a fixture, a parameter-set or perhaps a decorator like I do find (2) and (2.1) to be slightly unsavory. It makes tests bulkier and splits them across two units of code, but perhaps what I find the most unsavory is - that the idiomatic mechanism for passing parameters in Python should be function args, while here we'd be forfeiting it to dependency injection and relegating parameters/arguments to a second-tier mechanism. Another rationale for (2.1) is type-checking. Perhaps effort should be instead put into implementing a pytest plugin for mypy, which would impose the test function's type annotations on the parameters passed to I'd love to hear more of your experiences with complex tests (involving parameters and fixtures) and hear what you've been doing to keep it maintainable. |
i would propose experimentations with other ways to spell not just pram, but parametrize outside of pytest so we can have experimentation, its very easy to get things wrongly involved and i still have some horrors from the mark smearing, (and marks are still not sanely represented ) |
To me, the biggest maintainability issues with parametrized tests are (i) long parameter lists end up dwarfing the test code and (ii) python syntax is not very conducive to data entry (e.g. multiline-string parameters are always hard to read). I wrote a package called |
I came here looking for ultimately the same thing: a way to make passing long lists of params to parametrize more readable (without creating a custom class each time). With many params per test case, it can be hard to keep track of which param (by index) aligns with which of the function arguments. Another thing I thought would be nice is if you could use def dict_parametrize(test_cases: list[dict]):
keys = list(test_cases[0].keys())
params = [list(d.values()) for d in test_cases]
return pytest.mark.parametrize(keys, params)
@dict_parametrize(
[
{
"a": 1,
"b": 2,
"c": 3,
},
{
"a": 2,
"b": 2,
"c": 3,
},
]
)
def test(a, b, c):
assert a == 1 and b == 2 and c == 3 (I know there's a lot this doesn't account for, just throwing it out there) Edit: after digging around, looks like something like this was already suggested in the issue @The-Compiler linked to, ex: #7568 (comment) |
Popping in here to say I wrote a plugin which solves for this limitation as well after submitting my own issue (#10518): https://github.com/seandstewart/pytest-parametrize-suite @pytest.mark.suite(
case1=dict(arg1=1, arg2=2),
case2=dict(arg1=1, arg2=2),
)
def test_valid_suite(arg1, arg2):
# Given
expected_result = arg1
# When
result = arg2 - arg1
# Then
assert result == expected_result
@pytest.mark.suite(
case1=dict(arg1="cross1"),
case2=dict(arg1="cross2"),
)
@pytest.mark.suite(
case3=dict(arg2="product1"),
case4=dict(arg2="product2"),
)
def test_suite_matrix(arg1, arg2):
# Given
combination = arg1 + arg2
# When
possible_combinations.remove(combination)
# Then
assert combination not in possible_combinations
possible_combinations = {
"cross1product1",
"cross1product2",
"cross2product1",
"cross2product2",
} Essentially, the top-level keyword argument is your test ID. Then you pass in a mapping of The plugin does the work of validating that the structure of all the mappings for a given marker are the same structure, then translates it into the standard parametrize input and passes it on to metafunc.parametrize. All-in-all, it's fairly simple and naive, but it results in parametrized tests which are extremely easy to read, reason about, and maintain while providing a clean test output. I don't say all this to advertise for myself. Rather, I think this interface is an improvement over the standard interface and would love to see a version of it accepted into pytest core. Until then, I have my plugin to bridge the gap. |
The proposed plugin seems to intentionally ignore pytest.param to make the example of the builtin way look as painful as possible, as such I'm taken back |
Apologies, that's definitely not my intent. I've got skin in the game only insomuch as I want a simple way to define these parameters. You're correct, I didn't include an example using pytest.param, but this is more-so because I've never seen it used beyond a few examples in the documentation. By and large, I've only really seen folks just use the I work in pytest everyday for my job and for my other side projects, and I reference the documentation frequently, all in all, I'm a huge fan. The use of |
I know namedtuple has been mentioned here but I wanted to post a namedtuple code example because IMO it's actually quite clean. from typing import NamedTuple
from pytest import mark
class SumTestCase(NamedTuple):
a: int
b: int
c: int
@mark.parametrize(
SumTestCase._fields,
[
SumTestCase(a=1, b=2, c=3),
SumTestCase(a=2, b=3, c=5),
SumTestCase(a=3, b=4, c=7),
],
)
def test_sum(a: int, b: int, c: int) -> None:
assert a + b == c One drawback here compared with param is the lack of |
In many of my tests I've been using
pytest.param(..., id="...")
as means of providing more readable identifiers for my tests inline, since otherwise with multiple parameters, the autogenerated id is hard to read.Some of my parameter-driven tests end up growing beyond 2-3 arguments and maintaining the parameters positionally becomes error-prone and hurts readability. The reader has to mentally "zip" a list of parameters (of a parameter set) to the parameter names defined earlier, and if you need to add a parameter, now you need to add them to each and every test, and if you want to insert it (e.g. to maintain grouping), you'd have to carefully count parameters in each of the sets.
One pattern I've been introducing in such cases is defining a "ParamSet" attrs base class:
From there, a test would look like:
Before we discuss specifics, is this something we'd want
pytest.param
to enable?To give an idea, I'd imagine usage like:
or something more typed like this: (I like the fact that the type hint enables better developer experience when working on the test)
The text was updated successfully, but these errors were encountered: