10

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'
susanna
  • 1,395
  • 3
  • 20
  • 32
  • Your test class is not derived from anything, right? Maybe you have a fixture named "param"? From the first glance, this looks ok. – MrBean Bremen Sep 03 '20 at 09:08
  • class Tests(unittest.TestCase): – susanna Sep 04 '20 at 02:37
  • Ah yes, I see. You are aware you have to run `pytest` and not `unittest`, right? – MrBean Bremen Sep 04 '20 at 02:48
  • I find that pytest parameter does not work with unittest. – susanna Sep 04 '20 at 03:34
  • 2
    It works with pytest - pytest knows about unittest, but unittest does not know about pytest. – MrBean Bremen Sep 04 '20 at 03:42
  • https://docs.pytest.org/en/stable/unittest.html – susanna Sep 04 '20 at 04:35
  • 10
    pytest features in unittest.TestCase subclasses The following pytest features work in unittest.TestCase subclasses: Marks: skip, skipif, xfail; Auto-use fixtures; The following pytest features do not work, and probably never will due to different design philosophies: Fixtures (except for autouse fixtures, see below); Parametrization; Custom hooks; – susanna Sep 04 '20 at 04:35
  • 3
    Ah, you are right - this does not work (should not write comments before really awake...). It will work if you don't derive your test class from anything. – MrBean Bremen Sep 04 '20 at 04:40

2 Answers2

1

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.
micromoses
  • 6,747
  • 2
  • 20
  • 29
0

I am having the same problem. It looks like the parametrize does not always work within a test class but does work on a standalone function.

starball
  • 20,030
  • 7
  • 43
  • 238
WestCoastProjects
  • 58,982
  • 91
  • 316
  • 560