Skip to content

Nicer way to do parametrized tests? #7790

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

Closed
ckp95 opened this issue Sep 24, 2020 · 2 comments
Closed

Nicer way to do parametrized tests? #7790

ckp95 opened this issue Sep 24, 2020 · 2 comments

Comments

@ckp95
Copy link

ckp95 commented Sep 24, 2020

Writing parametrized tests is pretty annoying. The parameter values have to be passed in as a list of anonymous tuples, so you have to rely on the order alone to figure out what's what, and the comma-delineated string for the parameter values is just asking for trouble. It also leads to a lot of indentation and useless bracket- or parentheses-only lines after it goes through a code formatter like black.

Example:

@pytest.mark.parametrize(
    argnames="flavour_prices,expected_revenue",
    argvalues=[
        (
            {
                "Vanilla": 1.50,
                "Strawberry": 1.80,
                "Chocolate": 1.80,
                "Caramel": 1.65,
            },
            1_200_000,
        ),
        (
            {
                "Vanilla": 1.25,
                "Strawberry": 1.55,
                "Chocolate": 1.65,
                "Caramel": 2.10,
            },
            1_350_000,
        ),
    ],
    ids=["Strategy A", "Strategy B",]
)
def test_ice_cream_projections(flavour_prices, expected_revenue):
    ... # test function here

And then, if you want labels for these, you have to pass in a separate list for their ids, and make sure to keep that properly aligned with the argnames and argvalues. Overall it just seems like an unnecessary amount of bother.

What I often find myself doing is writing some variant of this helper function:

class Case:
    def __init__(self, label=None, /, **kwargs):
        self.label = label
        self.kwargs = kwargs


def nicer_parametrize(*args):
    for case in args:
        if not isinstance(case, Case):
            raise TypeError(f"{case!r} is not an instance of Case")

    first_case = next(iter(args))
    first_attrs = first_case.kwargs.keys()
    argument_string = ",".join(sorted(list(first_attrs)))

    case_list = []
    ids_list = []
    for case in args:
        case_dict = case.kwargs
        attrs = case_dict.keys()

        if attrs != first_attrs:
            raise ValueError(
                f"Inconsistent argument signature: {first_case!r}, {case!r}"
            )

        case_tuple = tuple(value for key, value in sorted(list(case_dict.items())))
        case_list.append(case_tuple)
        ids_list.append(case.label)

    return pytest.mark.parametrize(
        argnames=argument_string, argvalues=case_list, ids=ids_list
    )

That lets me write the parametrized tests in a much nicer way:

@nicer_parametrize(
    Case(
        "Strategy A",
        flavour_prices={
            "Vanilla": 1.50,
            "Strawberry": 1.80,
            "Chocolate": 1.80,
            "Caramel": 1.65,
        },
        expected_revenue=1_200_000,
    ),
    Case(
        "Strategy B",
        flavour_prices={
            "Vanilla": 1.25,
            "Strawberry": 1.55,
            "Chocolate": 1.65,
            "Caramel": 2.10,
        },
        expected_revenue=1_350_000,
    ),
    # can also do just Case(**kwargs) if no label is needed
)
def test_ice_cream_projections(flavour_prices, expected_revenue):
   ...

There's one less layer of indentation, it's more explicit what each part means, the order of the named arguments don't matter, and it's impossible to get the names, values, and labels all mixed up.

It would be nice if something like this were an official part of pytest.

@The-Compiler
Copy link
Member

Also see #7568 (comment)

@nicoddemus
Copy link
Member

Thanks for writing up @ckp95,

There's a number of threads related to this topic, so I will close this in favor of those. Feel free to subscribe/comment on the others. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants