Skip to main content

Parametrize tests within unittest TestCases.

Project description

https://img.shields.io/github/actions/workflow/status/adamchainz/unittest-parametrize/main.yml?branch=main&style=for-the-badge https://img.shields.io/pypi/v/unittest-parametrize.svg?style=for-the-badge https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge pre-commit

Parametrize tests within unittest TestCases.

Installation

Install with:

python -m pip install unittest-parametrize

Python 3.7 to 3.11 supported.


Testing a Django project? Check out my book Speed Up Your Django Tests which covers loads of recommendations to write faster, more accurate tests.


Usage

The API mirrors @pytest.mark.parametrize as much as possible. (Even the name parametrize over the slightly more common parameterize with an extra “e”. Don’t get caught out by that…)

There are two steps to parametrize a test case:

  1. Use ParametrizedTestCase in the base classes for your test case.

  2. Apply @parametrize to any tests for parametrization. This decorator takes (at least):

    • the argument names to parametrize, as comma-separated string

    • a list of parameter tuples to create individual tests for

Here’s a basic example:

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            (1, 1),
            (2, 4),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

@parametrize modifies the class at definition time with Python’s __init_subclass__ hook. It removes the original test method and creates wrapped copies with individual names. Thus the parametrization should work regardless of the test runner you use (be it unittest, Django’s test runner, pytest, etc.).

Provide argument names as a string

If you need, you can provide argument names as a sequence of strings instead:

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        ("x", "expected"),
        [
            (1, 1),
            (2, 4),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

Custom test name suffixes

By default, test names are extended with an index, starting at zero. You can see these names when running the tests:

$ python -m unittest t.py -v
test_square_0 (t.SquareTests.test_square_0) ... ok
test_square_1 (t.SquareTests.test_square_1) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

You can customize these names by passing param objects, which contain the arguments plus an ID for the suffix:

from unittest_parametrize import param
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            param(1, 1, id="one"),
            param(2, 4, id="two"),
        ],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

Yielding perhaps more natural names:

$ python -m unittest t.py -v
test_square_one (t.SquareTests.test_square_one) ... ok
test_square_two (t.SquareTests.test_square_two) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Parameter IDs should be valid Python identifier suffixes.

Alternatively, you can provide the id’s separately with the ids argument:

from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class SquareTests(ParametrizedTestCase):
    @parametrize(
        "x,expected",
        [
            (1, 1),
            (2, 4),
        ],
        ids=["one", "two"],
    )
    def test_square(self, x: int, expected: int) -> None:
        self.assertEqual(x**2, expected)

Use with other test decorators

@parametrize tries to ensure it is the top-most (outermost) decorator. This limitation exists to ensure that the decorator applies to each test. So decorators like @mock.patch.object need be beneath @parametrize:

from unittest import mock
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class MockingTests(ParametrizedTestCase):
    @parametrize(
        "nails",
        [(1,), (2,)],
    )
    @mock.patch.object(board, "length", new=9001)
    def test_boarding(self, nails):
        ...

Multiple @parametrize decorators

@parametrize is not stackable. To create a cross-product of tests, use itertools.product():

from itertools import product
from unittest_parametrize import parametrize
from unittest_parametrize import ParametrizedTestCase


class RocketTests(ParametrizedTestCase):
    @parametrize(
        "use_ions,hyperdrive_level,nose_colour",
        list(
            product(
                [True, False],
                [0, 1, 2],
                ["red", "yellow"],
            )
        ),
    )
    def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
        ...

The above creates 2 * 3 * 2 = 12 versions of test_takeoff.

Use ParametrizedTestCase in your base test case class

ParametrizedTestCase does nothing if there aren’t any @parametrize-decorated tests within a class. Therefore you can include it in your project’s base test case class so that @parametrize works immediately in all test cases.

For example, within a Django project, you can create a set of project-specific base test case classes extending those provided by Django. You can do this in a module like example.test, and use the base classes throughout your test suite. To add ParametrizedTestCase to all your copies, use it in a custom SimpleTestCase and then mixin to others using multiple inheritance like so:

from django import test
from unittest_parametrize import ParametrizedTestCase


class SimpleTestCase(ParametrizedTestCase, test.SimpleTestCase):
    pass


class TestCase(SimpleTestCase, test.TestCase):
    pass


class TransactionTestCase(SimpleTestCase, test.TransactionTestCase):
    pass


class LiveServerTestCase(SimpleTestCase, test.LiveServerTestCase):
    pass

History

When I started writing unit tests, I learned to use DDT (Data-Driven Tests) for parametrizing tests. It works, but the docs are a bit thin, and the API a little obscure (what does @ddt stand for again?).

Later when picking up pytest, I learned to use its parametrization API. It’s legible and flexible, but it doesn’t work with unittest test cases, which Django’s test tooling provides.

So, until the creation of this package, I was using parameterized on my (Django) test cases. This package supports parametrization across multiple test runners, though most of them are “legacy” by now.

I created unittest-parametrize as a smaller alternative to parameterized, with these goals:

  1. Only support unittest test cases. For other types of test, you can use pytest’s parametrization.

  2. Avoid any custom test runner support. Modifying the class at definition time means that all test runners will see the tests the same.

  3. Use modern Python features like __init_subclass__.

  4. Have full type hint coverage. You shouldn’t find unittest-parametrize a blocker when adopting Mypy with strict mode on.

  5. Use the name “parametrize” rather than “parameterize”. This unification of spelling with pytest should help reduce confusion around the extra “e”.

Thanks to the creators and maintainers of ddt, parameterized, and pytest for their hard work.

Why not subtests?

TestCase.subTest() is unittest’s built-in “parametrization” solution. You use it in a loop within a single test method:

from unittest import TestCase


class SquareTests(TestCase):
    def test_square(self):
        tests = [
            (1, 1),
            (2, 4),
        ]
        for x, expected in tests:
            with self.subTest(x=x):
                self.assertEqual(x**2, expected)

This approach crams multiple actual tests into one test method, with several consequences:

  • If a subtest fails, it prevents the next subtests from running. Thus, failures are harder to debug, since each test run can only give you partial information.

  • Subtests can leak state. Without correct isolation, they may not test what they appear to.

  • Subtests cannot be reordered by tools that detect state leakage, like pytest-randomly.

  • Subtests skew test timings, since the test method runs multiple tests.

  • Everything is indented two extra levels for the loop and context manager.

Parametrization avoids all these issues by creating individual test methods.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unittest_parametrize-1.0.0.tar.gz (9.4 kB view details)

Uploaded Source

Built Distribution

unittest_parametrize-1.0.0-py3-none-any.whl (7.6 kB view details)

Uploaded Python 3

File details

Details for the file unittest_parametrize-1.0.0.tar.gz.

File metadata

  • Download URL: unittest_parametrize-1.0.0.tar.gz
  • Upload date:
  • Size: 9.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.2

File hashes

Hashes for unittest_parametrize-1.0.0.tar.gz
Algorithm Hash digest
SHA256 fdc3070c97065dc64d7882fd29eb456744bca726101b2984ed634cf23fce19de
MD5 9188cb790d9f985624ec25b2539ea2f8
BLAKE2b-256 1761c62e35a71796d84ff4be5d64a9796e7ff95015607078e32a0296812f65c1

See more details on using hashes here.

File details

Details for the file unittest_parametrize-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for unittest_parametrize-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b0fc88d4e8f812f862e38235f00b397441f5cc1f322d013d9c652a175874cfa4
MD5 91687eccc8139cf34958976f69f095c3
BLAKE2b-256 3ebd545b6a0cc80035c9b0bac4e0b6ad4138b73a4cac4d9a754f1564f748cab0

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page