diff --git a/parameterized/parameterized.py b/parameterized/parameterized.py index 969a157..cc6314b 100644 --- a/parameterized/parameterized.py +++ b/parameterized/parameterized.py @@ -19,6 +19,11 @@ class SkipTest(Exception): pass +try: + import pytest +except ImportError: + pytest = None + PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 @@ -352,6 +357,94 @@ def __init__(self, input, doc_func=None, skip_on_empty=False): def __call__(self, test_func): self.assert_not_in_testcase_subclass() + input = self.get_input() + wrapper = self._wrap_test_func(test_func, input) + wrapper.parameterized_input = input + wrapper.parameterized_func = test_func + test_func.__name__ = "_parameterized_original_%s" %(test_func.__name__, ) + + return wrapper + + def _wrap_test_func(self, test_func, input): + """ Wraps a test function so that it will appropriately handle + parameterization. + + In the general case, the wrapper will enumerate the input, yielding + test cases. + + In the case of pytest4, the wrapper will use + ``@pytest.mark.parametrize`` to parameterize the test function. """ + + if not input: + if not self.skip_on_empty: + raise ValueError( + "Parameters iterable is empty (hint: use " + "`parameterized([], skip_on_empty=True)` to skip " + "this test when the input is empty)" + ) + return wraps(test_func)(skip_on_empty_helper) + + if pytest and pytest.__version__ > '4.0.0': + Undefined = object() + test_func_wrapped = test_func + test_func_real, mock_patchings = unwrap_mock_patch_func(test_func_wrapped) + func_argspec = getargspec(test_func_real) + + func_args = func_argspec.args + if mock_patchings: + func_args = func_args[:-len(mock_patchings)] + + func_args_no_self = func_args + if func_args_no_self[:1] == ["self"]: + func_args_no_self = func_args_no_self[1:] + + args_with_default = dict( + (arg, Undefined) + for arg in func_args_no_self + ) + for (arg, default) in zip(reversed(func_args_no_self), reversed(func_argspec.defaults or [])): + args_with_default[arg] = default + + pytest_params = [] + for i in input: + p = dict(args_with_default) + for (arg, val) in zip(func_args_no_self, i.args): + p[arg] = val + p.update(i.kwargs) + + # Sanity check: all arguments should now be defined + if any(v is Undefined for v in p.values()): + raise ValueError( + "When parameterizing function %r: no value for arguments: %s" %( + test_func, + ", ".join( + repr(arg) + for (arg, val) in p.items() + if val is Undefined + ), + ) + ) + + pytest_params.append(pytest.param(*[ + p.get(arg) for arg in func_args_no_self + ])) + + namespace = { + "__test_func": test_func_wrapped, + } + wrapper_name = "parameterized_pytest_wrapper_%s" %(test_func.__name__, ) + exec( + "def %s(%s): return __test_func(%s)" %( + wrapper_name, + ",".join(func_args), + ",".join(func_args), + ), + namespace, + namespace, + ) + + return pytest.mark.parametrize(",".join(func_args_no_self), pytest_params)(namespace[wrapper_name]) + @wraps(test_func) def wrapper(test_self=None): test_cls = test_self and type(test_self) @@ -366,7 +459,7 @@ def wrapper(test_self=None): ) %(test_self, )) original_doc = wrapper.__doc__ - for num, args in enumerate(wrapper.parameterized_input): + for num, args in enumerate(input): p = param.from_decorator(args) unbound_func, nose_tuple = self.param_as_nose_tuple(test_self, test_func, num, p) try: @@ -383,21 +476,6 @@ def wrapper(test_self=None): if test_self is not None: delattr(test_cls, test_func.__name__) wrapper.__doc__ = original_doc - - input = self.get_input() - if not input: - if not self.skip_on_empty: - raise ValueError( - "Parameters iterable is empty (hint: use " - "`parameterized([], skip_on_empty=True)` to skip " - "this test when the input is empty)" - ) - wrapper = wraps(test_func)(skip_on_empty_helper) - - wrapper.parameterized_input = input - wrapper.parameterized_func = test_func - test_func.__name__ = "_parameterized_original_%s" %(test_func.__name__, ) - return wrapper def param_as_nose_tuple(self, test_self, func, num, p): @@ -618,6 +696,11 @@ def decorator(base_class): return decorator +def unwrap_mock_patch_func(f): + if not hasattr(f, "patchings"): + return (f, []) + real_func, patchings = unwrap_mock_patch_func(f.__wrapped__) + return (real_func, patchings + f.patchings) def get_class_name_suffix(params_dict): if "name" in params_dict: diff --git a/parameterized/test.py b/parameterized/test.py index f98d865..0b9ebe4 100644 --- a/parameterized/test.py +++ b/parameterized/test.py @@ -40,6 +40,7 @@ def expect(skip, tests=None): test_params = [ (42, ), + (42, "bar_val"), "foo0", param("foo1"), param("foo2", bar=42), @@ -50,6 +51,7 @@ def expect(skip, tests=None): "test_naked_function('foo1', bar=None)", "test_naked_function('foo2', bar=42)", "test_naked_function(42, bar=None)", + "test_naked_function(42, bar='bar_val')", ]) @parameterized(test_params) @@ -63,6 +65,7 @@ class TestParameterized(object): "test_instance_method('foo1', bar=None)", "test_instance_method('foo2', bar=42)", "test_instance_method(42, bar=None)", + "test_instance_method(42, bar='bar_val')", ]) @parameterized(test_params) @@ -95,10 +98,16 @@ def test_setup(self, count, *a): missing_tests.remove("test_setup(%s)" %(self.actual_order, )) -def custom_naming_func(custom_tag): +def custom_naming_func(custom_tag, kw_name): def custom_naming_func(testcase_func, param_num, param): - return testcase_func.__name__ + ('_%s_name_' % custom_tag) + str(param.args[0]) - + return ( + testcase_func.__name__ + + '_%s_name_' %(custom_tag, ) + + str(param.args[0]) + + # This ... is a bit messy, to properly handle the values in + # `test_params`, but ... it should work. + '_%s' %(param.args[1] if len(param.args) > 1 else param.kwargs.get(kw_name), ) + ) return custom_naming_func @@ -214,6 +223,7 @@ class TestParamerizedOnTestCase(TestCase): "test_on_TestCase('foo1', bar=None)", "test_on_TestCase('foo2', bar=42)", "test_on_TestCase(42, bar=None)", + "test_on_TestCase(42, bar='bar_val')", ]) @parameterized.expand(test_params) @@ -221,20 +231,21 @@ def test_on_TestCase(self, foo, bar=None): missing_tests.remove("test_on_TestCase(%r, bar=%r)" %(foo, bar)) expect([ - "test_on_TestCase2_custom_name_42(42, bar=None)", - "test_on_TestCase2_custom_name_foo0('foo0', bar=None)", - "test_on_TestCase2_custom_name_foo1('foo1', bar=None)", - "test_on_TestCase2_custom_name_foo2('foo2', bar=42)", + "test_on_TestCase2_custom_name_42_None(42, bar=None)", + "test_on_TestCase2_custom_name_42_bar_val(42, bar='bar_val')", + "test_on_TestCase2_custom_name_foo0_None('foo0', bar=None)", + "test_on_TestCase2_custom_name_foo1_None('foo1', bar=None)", + "test_on_TestCase2_custom_name_foo2_42('foo2', bar=42)", ]) @parameterized.expand(test_params, - name_func=custom_naming_func("custom")) + name_func=custom_naming_func("custom", "bar")) def test_on_TestCase2(self, foo, bar=None): stack = inspect.stack() frame = stack[1] frame_locals = frame[0].f_locals nose_test_method_name = frame_locals['a'][0]._testMethodName - expected_name = "test_on_TestCase2_custom_name_" + str(foo) + expected_name = "test_on_TestCase2_custom_name_" + str(foo) + "_" + str(bar) assert_equal(nose_test_method_name, expected_name, "Test Method name '%s' did not get customized to expected: '%s'" % (nose_test_method_name, expected_name))