Package: unittest-parametrize
Parametrize tests within unittest TestCases.InstallationInstall with:python-mpipinstallunittest-parametrizePython 3.8 to 3.12 supported.Testing a Django project?Check out my bookSpeed Up Your Django Testswhich covers loads of recommendations to write faster, more accurate tests.UsageThe API mirrors@pytest.mark.parametrizeas much as possible.
(Even the nameparametrizeover the slightly more commonparameterizewith an extra “e”.
Don’t get caught out by that…)There are two steps to parametrize a test case:UseParametrizedTestCasein the base classes for your test case.Apply@parametrizeto any tests for parametrization.
This decorator takes (at least):the argument names to parametrize, as comma-separated stringa list of parameter tuples to create individual tests forHere’s a basic example:fromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassSquareTests(ParametrizedTestCase):@parametrize("x,expected",[(1,1),(2,4),],)deftest_square(self,x:int,expected:int)->None:self.assertEqual(x**2,expected)@parametrizemodifies 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 separate stringsYou can provide argument names as a sequence of strings instead:fromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassSquareTests(ParametrizedTestCase):@parametrize(("x","expected"),[(1,1),(2,4),],)deftest_square(self,x:int,expected:int)->None:self.assertEqual(x**2,expected)UseParametrizedTestCasein your base test case classParametrizedTestCasedoes 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@parametrizeworks immediately in all test cases.For example, within a Django project, you can create a set of project-specific base test case classes extendingthose provided by Django.
You can do this in a module likeexample.test, and use the base classes throughout your test suite.
To addParametrizedTestCaseto all your copies, use it in a customSimpleTestCaseand then mixin to others using multiple inheritance like so:fromdjangoimporttestfromunittest_parametrizeimportParametrizedTestCaseclassSimpleTestCase(ParametrizedTestCase,test.SimpleTestCase):passclassTestCase(SimpleTestCase,test.TestCase):passclassTransactionTestCase(SimpleTestCase,test.TransactionTestCase):passclassLiveServerTestCase(SimpleTestCase,test.LiveServerTestCase):passCustom test name suffixesBy default, test names are extended with an index, starting at zero.
You can see these names when running the tests:$python-munittestt.py-vtest_square_0 (t.SquareTests.test_square_0) ... ok
test_square_1 (t.SquareTests.test_square_1) ... ok

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

OKYou can customize these names by passingparamobjects, which contain the arguments and an optional ID for the suffix:fromunittest_parametrizeimportparamfromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassSquareTests(ParametrizedTestCase):@parametrize("x,expected",[param(1,1,id="one"),param(2,4,id="two"),],)deftest_square(self,x:int,expected:int)->None:self.assertEqual(x**2,expected)Yielding perhaps more natural names:$python-munittestt.py-vtest_square_one (t.SquareTests.test_square_one) ... ok
test_square_two (t.SquareTests.test_square_two) ... ok

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

OKParameter IDs should be valid Python identifier suffixes.Since parameter IDs are optional, you can provide them only for some tests:fromunittest_parametrizeimportparamfromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassSquareTests(ParametrizedTestCase):@parametrize("x,expected",[param(1,1),param(20,400,id="large"),],)deftest_square(self,x:int,expected:int)->None:self.assertEqual(x**2,expected)ID-freeparams fall back to the default index suffixes:$python-munittestt.py-vtest_square_0 (example.SquareTests.test_square_0) ... ok
test_square_large (example.SquareTests.test_square_large) ... ok

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

OKAlternatively, you can provide the id’s separately with theidsargument:fromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassSquareTests(ParametrizedTestCase):@parametrize("x,expected",[(1,1),(2,4),],ids=["one","two"],)deftest_square(self,x:int,expected:int)->None:self.assertEqual(x**2,expected)Use with other test decorators@parametrizetries to ensure it is the top-most (outermost) decorator.
This limitation exists to ensure that other decorators apply to each parametrized test.
So decorators like@mock.patchneed be beneath@parametrize:fromunittestimportmockfromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassCarpentryTests(ParametrizedTestCase):@parametrize("nails",[(11,),(17,)],)@mock.patch("example.hammer",autospec=True)deftest_nail_a_board(self,mock_hammer,nails):...Also note that due to howmock.patchalways adds positional arguments at the start, the parametrized arguments must come last.@parametrizealways adds parameters as keyword arguments, so you can also usekeyword-only syntaxfor parametrized arguments:# ...deftest_nail_a_board(self,mock_hammer,*,nails):...Multiple@parametrizedecorators@parametrizeis not stackable.
To create a cross-product of tests, you can use nested list comprehensions:fromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassRocketTests(ParametrizedTestCase):@parametrize("use_ions,hyperdrive_level",[(use_ions,hyperdrive_level)foruse_ionsin[True,False]forhyperdrive_levelin[0,1,2]],)deftest_takeoff(self,use_ions,hyperdrive_level)->None:...The above creates 2 * 3 = 6 versions oftest_takeoff.For larger combinations,itertools.product()may be more readable:fromitertoolsimportproductfromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseclassRocketTests(ParametrizedTestCase):@parametrize("use_ions,hyperdrive_level,nose_colour",list(product([True,False],[0,1,2],["red","yellow"],)),)deftest_takeoff(self,use_ions,hyperdrive_level,nose_colour)->None:...The above creates 2 * 3 * 2 = 12 versions oftest_takeoff.Parametrizing multiple tests in a test case@parametrizeonly works as a function decorator, not a class decorator.
To parametrize all tests within a test case, create a separate decorator and apply it to each method:fromunittest_parametrizeimportparametrizefromunittest_parametrizeimportParametrizedTestCaseparametrize_race=parametrize("race",[("Human",),("Halfling",),("Dwarf",),("Elf",)],)classStatsTests(ParametrizedTestCase):@parametrize_racedeftest_strength(self,race:str)->None:...@parametrize_racedeftest_dexterity(self,race:str)->None:......HistoryWhen I started writing unit tests, I learned to useDDT (Data-Driven Tests)for parametrizing tests.
It works, but the docs are a bit thin, and the API a little obscure (what does@ddtstand for again?).Later when picking up pytest, I learned to use itsparametrization 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 usingparameterizedon 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 toparameterized, with these goals:Only support unittest test cases.
For other types of test, you can use pytest’s parametrization.Avoid any custom test runner support.
Modifying the class at definition time means that all test runners will see the tests the same.Use modern Python features like__init_subclass__.Have full type hint coverage.
You shouldn’t find unittest-parametrize a blocker when adopting Mypy with strict mode on.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:fromunittestimportTestCaseclassSquareTests(TestCase):deftest_square(self):tests=[(1,1),(2,4),]forx,expectedintests:withself.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, likepytest-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.