I try to add use pytest parametrize for my unit tests. But I see the error of "missing required positional arguments". Can you help where the issue is? Thanks.
@pytest.mark.parametrize("param", ["a", "b"])
def test_pytest(self, param):
print(param)
assert False
I get the following exception:
class Tests(unittest.TestCase):
@contextlib.contextmanager
def testPartExecutor(self, test_case, isTest=False):
old_success = self.success
self.success = True
try:
> yield
with outcome.testPartExecutor(self):
self.setUp()
if outcome.success:
outcome.expecting_failure = expecting_failure
with outcome.testPartExecutor(self, isTest=True):
> testMethod()
E TypeError: test_pytest() missing 1 required positional argument: 'param'
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:
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 ====================================
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 =======================================
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.
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.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.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With