Skip to content

Nicer way to do parametrized tests? #7790

Closed
@ckp95

Description

@ckp95

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions