Code Monkey home page Code Monkey logo

pytest-parametrize-cases's Introduction

pytest-parametrize-cases

TestsPyPI

What is it?

The way parametrized tests work in pytest annoys me:

import pytest


@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expecteds

Passing in argument names as a comma-separated string just looks ugly and un-Pythonic, and you have to give each case as an anonymous tuple and make sure to get the order right every time. This becomes harder the more parameters you have. It's also a bit awkward to specify the test IDs. Even when you use pytest.param there's still nothing stopping you from getting the order of the parameters wrong.

So, I wrote a simple wrapper around pytest.mark.parametrize which makes it a bit nicer to read and use. The previous example would be written:

from pytest_parametrize_cases import Case, parametrize_cases


@parametrize_cases(
    Case(test_input="3+5", expected=8),
    Case(test_input="2+4", expected=6),
    Case(test_input="6*9", expected=42)
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

It also supports optional test IDs. Using the same example as the one in the pytest docs for pytest.mark.parametrize, this is how you include them:

from datetime import datetime, timedelta

from pytest_parametrize_cases import Case, parametrize_cases

@parametrize_cases(
    Case(
        "forward",
        a=datetime(2001, 12, 12),
        b=datetime(2001, 12, 11),
        expected=timedelta(1),
    ),
    Case(
        "backward",
        a=datetime(2001, 12, 11),
        b=datetime(2001, 12, 12),
        expected=timedelta(-1),
    ),
)
def test_timedistance_v4(a, b, expected):
    diff = a - b
    assert diff == expected

That is to say, you decorate your test function with @parametrize_cases and pass in multiple instances of Case as arguments. The arguments to the Case constructor are [optionally] a positional-only string which constitutes the test ID (printed when you use the -v / --verbose flag), and then keyword arguments for each of the parameters the test function expects.

In my opinion this is much more readable and user-friendly than the default way of writing parametrized tests. Related data is kept together rather than spread out in multiple containers. Specifying the keyword arguments is mandatory, so it is always clear where each piece of data ends up in the test function (explicit is better than implicit). And it is more convenient to specify test IDs.

The parametrize_cases decorator can be stacked multiple times to give the Cartesian product of your parametrizations, in the exact same way as mark.parametrize.

Dependencies

  • Python >= 3.8
  • pytest

Installation

pip install pytest-parametrize-cases

Or just look in src/pytest_parametrize_cases/case.py and copypaste the two definitions to somewhere in your test suite.

pytest-parametrize-cases's People

Contributors

ckp95 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pytest-parametrize-cases's Issues

Make parametrize-cases a plugin for pytest

Hey, have you though about making this installable as a pytest plugin?

I think you have a really helpful bit of code here which I've been using everywhere to make my tests look great. Not only that, it makes them easy to extend to new cases. I'm not certain on what the advantages of making this a plugin are yet, but I think this solution deserves to get a bit more traction than it has - perhaps being on the plug in list can help?

I'm also coming back around to the thought I've writing a new article in my test series focusing on parametrisation using paramterize_cases.

Interested in your thoughts, and possibly adding some support.

Add handling for variable number of kwargs among the test cases

I've been using the code for parametrize_cases and have made a couple of extensions that you may want to adopt into the main source code. Sometimes you want to alter a default value in one of your test cases without having to specify the default in all the remaining test cases. This saves on code repetition, but may sacrifice some explicitness. It's something I have rarely used but is nice to have.

Requirements:

  • If a kwarg is present in one Case, but not in another, then that kwarg is initialised as None for the Case where it is not specified (default behaviour)
  • Optional behaviour to set default values for specific kwargs other than None, to be used when not included in the Case.

I've implemented something currently which doesn't make any breaking changes to the API i.e. it's back-portable. It involved
first updating the call-signature of parametrize_cases with a defaults parameter.

def parametrize_cases(*cases: Case, defaults: Mapping[str, Any] = None) -> MarkDecorator:

And then gathering all the args when the Cases are looped through at the start, replacing the current "Inconsistent parametrization" check taking place:

        if not defaults:
            defaults = {}
        
        all_args = set()
        for case in cases:
            if not isinstance(case, Case):
                raise TypeError(f"{case!r} is not an instance of Case")
        
            all_args.update(case.kwargs.keys())

Before working out the diff, and updating the original case kwargs with any missing kwargs initialised with their default values.

    for case in cases:
        case_kwargs = case.kwargs.copy(deep=True)
        args = case.kwargs.keys()

        # Make sure all keys are in each case, otherwise initialise with None.
        diff = {k: defaults.get(k, None) for k in set(all_args) - set(args)}
        case_kwargs.update(diff)

I made a few additional refactors as part of this change to express the intention of the code better (imo). Happy to take this change forward with a Pull Request.

Test cases:

  1. That parametrize_cases successfully runs tests for two test cases with different kwargs, replacing the missing kwargs with None.
  2. That a specific kwarg can be initialised with a different default value other than None.

Interested to hear any thoughts you have. As with the other issue I require a bit of guidance on how to create the tests outside of pytest.

Add handling of pytest marks to the decorator

I've been using the code for parametrize_cases and have made a couple of extensions that you may want to adopt into the main source code. Sometimes you may want to mark your test cases individually with pytest.mark.skip or pytest.mark.xfail for example. This issue will implement the handling of marks on a case by case basis.

The first one I made was to add a marks parameter (marks: Optional[MarkDecorator] = None) to the Case class, and then wrapping each case tuple in the pytest.param function, passing in the marks.

# If marks are given, wrap the case tuple.
if case.marks:
    case_tuple = pytest.param(*case_tuple, marks=case.marks)

This required a couple of other small refactors to express the intention of the code better. Happy to take this issue forward on a Pull Request but I may need some help understanding how to test pytest functionality when all I know is testing within pytest. I can see you have nox setup, can you provide any guidance there?

Test cases:

  1. That an individual test is skipped, when marked with pytest.mark.skip
  2. That an individual test reports as xfail when marked with pytest.mark.xfail

Support for pytest >= 7.0.0

This package is not compatible with pytest >= 7.0.0. Would be great to have support for pytest >= 7.0.0. Sample error message below:

The conflict is caused by:
The user requested pytest==7.0.1
pytest-parametrize-cases 0.1.1 depends on pytest<7.0.0 and >=6.1.2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.