As quoted in a comment to the question from pytest's documentation, pytest's parametrization does not work for methods under unittest.TestCase
[sub]classes. I can think of several approaches to overcome this issue:
First approach - find someone else who already solved this issue
One example for this would be the parameterized package, which can be used as follows:
import unittest
from parameterized import parameterized
class TestClass(unittest.TestCase):
def test_ok(self):
assert True
@parameterized.expand((("a",), ("b",)))
def test_single(self, param):
print(param)
assert False
@parameterized.expand((("a", "c", "d"), ("b", "c", "d")))
def test_multiple(self, first, second, third):
self.assertEqual(second, "c")
self.assertEqual(third, "d")
self.assertTrue(first in ("a", "b"))
self.fail(f"Failing test for {first}_{second}_{third}")
pytest will handle the code above as expected, resulting with the following summary:
====================================== short test summary info ======================================
FAILED test.py::TestClass::test_multiple_0_a - AssertionError: Failing test for a_c_d
FAILED test.py::TestClass::test_multiple_1_b - AssertionError: Failing test for b_c_d
FAILED test.py::TestClass::test_single_0_a - assert False
FAILED test.py::TestClass::test_single_1_b - assert False
==================================== 4 failed, 1 passed in 0.06s ====================================
Second approach - create explicit test for each param
While this scales-up quite poorly (e.g. when testing for more than a couple of values or parameters), explicit declarations avoid introducing a new 3rd-party package.
The more cumbersome approach with explicit methods:
import unittest
class TestClass(unittest.TestCase):
def _test_param(self, param):
self.assertEqual(param, list(range(5)))
def test_0_to_4(self):
return self._test_param([0, 1, 2, 3, 4])
def test_sort_list(self):
return self._test_param(sorted([0, 3, 2, 1, 4]))
def test_fail(self):
return self._test_param([0, 3, 2, 1, 4])
And a leaner approach using partialmethods:
import unittest
from functools import partialmethod
class TestClass(unittest.TestCase):
def _test_param(self, param):
self.assertEqual(param, list(range(5)))
test_0_to_4 = partialmethod(_test_param, [0, 1, 2, 3, 4])
test_sort_list = partialmethod(_test_param, sorted([0, 3, 2, 1, 4]))
test_fail = partialmethod(_test_param, [0, 3, 2, 1, 4])
While these would yield different tracebacks, pytest's summary will be the same for both:
========================================= short test summary info =========================================
FAILED test.py::TestClass::test_fail - AssertionError: Lists differ: [0, 3, 2, 1, 4] != [0, 1, 2, 3, 4]
======================================= 1 failed, 2 passed in 0.02s =======================================
Third approach - hack it yourself
As an elegant decorator interface can already be found with the first method, I will demonstrate a hacky, clunky, unreusable, but fairly quick DIY solution; injecting the class with "dynamic" test methods, attaching the parameters as kwargs to each method:
import unittest
class TestClass(unittest.TestCase):
def test_ok(self):
assert True
def _test_injected_method(self, first, second):
self.assertEqual(second, "z", f"{second} is not z")
self.assertTrue(first in ("x", "y"), f"{first} is not x or y")
self.fail(f"Failing test for {first}_{second}")
for param1 in ("w", "x", "y"):
for param2 in ("z", "not_z"):
def _test(self, p1=param1, p2=param2):
return self._test_injected_method(p1, p2)
_test.__name__ = f"test_injected_method_{param1}_{param2}"
setattr(TestClass, _test.__name__, _test)
This will also work with pytest, yielding the following summary:
========================================= short test summary info =========================================
FAILED test.py::TestClass::test_injected_method_w_not_z - AssertionError: 'not_z' != 'z'
FAILED test.py::TestClass::test_injected_method_w_z - AssertionError: False is not true : w is not x or y
FAILED test.py::TestClass::test_injected_method_x_not_z - AssertionError: 'not_z' != 'z'
FAILED test.py::TestClass::test_injected_method_x_z - AssertionError: Failing test for x_z
FAILED test.py::TestClass::test_injected_method_y_not_z - AssertionError: 'not_z' != 'z'
FAILED test.py::TestClass::test_injected_method_y_z - AssertionError: Failing test for y_z
======================================= 6 failed, 1 passed in 0.02s =======================================
IMHO this approach is less elegant than the second (which IMO is less elegant than the first), but if introducing 3rd-party packages is not an option, this approach scales-up more efficiently than the second.
Other approaches
- Refactoring test classes to not inherit from
unittest.TestCase
is an option, but quite a risky one; irregardless of technical efforts required for this task (e.g. replacing all self.assert*
calls with assert
statements, handling setUp
and tearDown
methods, etc.), there is also an implicit risk where tests could be silently ignored unless explicitly invoked with pytest.
- Other solutions are suggested here and there, but I believe most of them address
nose
rather than pytest
. Some answers suggest using metaclasses, and while I have a feeling that's an overkill of a solution to this specific problem, if my previous suggestions do not fit your use-case - that's something worth looking into.