diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0bc0e0102..8a8389548 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -159,6 +159,10 @@ time or change existing behaviors in order to make them less surprising/more use automatically generated id for that argument will be used. Thanks `@palaviv`_ for the complete PR (`#1468`_). +* The parameter to xunit-style setup/teardown methods (``setup_method``, + ``setup_module``, etc.) is now optional and may be omitted. + Thanks `@okken`_ for bringing this to attention and `@nicoddemus`_ for the PR. + * Improved automatic id generation selection in case of duplicate ids in parametrize. Thanks `@palaviv`_ for the complete PR (`#1474`_). @@ -308,6 +312,7 @@ time or change existing behaviors in order to make them less surprising/more use .. _@nikratio: https://github.com/nikratio .. _@novas0x2a: https://github.com/novas0x2a .. _@obestwalter: https://github.com/obestwalter +.. _@okken: https://github.com/okken .. _@olegpidsadnyi: https://github.com/olegpidsadnyi .. _@omarkohl: https://github.com/omarkohl .. _@palaviv: https://github.com/palaviv diff --git a/_pytest/python.py b/_pytest/python.py index 4a4cd5dc4..fb374381d 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -25,10 +25,6 @@ cutdir2 = py.path.local(_pytest.__file__).dirpath() cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) -def _has_positional_arg(func): - return func.__code__.co_argcount - - def filter_traceback(entry): # entry.path might sometimes return a str object when the entry # points to dynamically generated code @@ -439,34 +435,51 @@ class Module(pytest.File, PyCollector): "decorator) is not allowed. Use @pytest.mark.skip or " "@pytest.mark.skipif instead." ) - #print "imported test module", mod self.config.pluginmanager.consider_module(mod) return mod def setup(self): - setup_module = xunitsetup(self.obj, "setUpModule") + setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule") if setup_module is None: - setup_module = xunitsetup(self.obj, "setup_module") + setup_module = _get_xunit_setup_teardown(self.obj, "setup_module") if setup_module is not None: - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, its probably a pytest style one - # so we pass the current module object - if _has_positional_arg(setup_module): - setup_module(self.obj) - else: - setup_module() - fin = getattr(self.obj, 'tearDownModule', None) - if fin is None: - fin = getattr(self.obj, 'teardown_module', None) - if fin is not None: - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, it's probably a pytest style one - # so we pass the current module object - if _has_positional_arg(fin): - finalizer = lambda: fin(self.obj) - else: - finalizer = fin - self.addfinalizer(finalizer) + setup_module() + + teardown_module = _get_xunit_setup_teardown(self.obj, 'tearDownModule') + if teardown_module is None: + teardown_module = _get_xunit_setup_teardown(self.obj, 'teardown_module') + if teardown_module is not None: + self.addfinalizer(teardown_module) + + +def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): + """ + Return a callable to perform xunit-style setup or teardown if + the function exists in the ``holder`` object. + The ``param_obj`` parameter is the parameter which will be passed to the function + when the callable is called without arguments, defaults to the ``holder`` object. + Return ``None`` if a suitable callable is not found. + """ + param_obj = param_obj if param_obj is not None else holder + result = _get_xunit_func(holder, attr_name) + if result is not None: + arg_count = result.__code__.co_argcount + if inspect.ismethod(result): + arg_count -= 1 + if arg_count: + return lambda: result(param_obj) + else: + return result + + +def _get_xunit_func(obj, name): + """Return the attribute from the given object to be used as a setup/teardown + xunit-style function, but only if not marked as a fixture to + avoid calling it twice. + """ + meth = getattr(obj, name, None) + if fixtures.getfixturemarker(meth) is None: + return meth class Class(PyCollector): @@ -479,7 +492,7 @@ class Class(PyCollector): return [self._getcustomclass("Instance")(name="()", parent=self)] def setup(self): - setup_class = xunitsetup(self.obj, 'setup_class') + setup_class = _get_xunit_func(self.obj, 'setup_class') if setup_class is not None: setup_class = getattr(setup_class, 'im_func', setup_class) setup_class = getattr(setup_class, '__func__', setup_class) @@ -523,12 +536,12 @@ class FunctionMixin(PyobjMixin): else: setup_name = 'setup_function' teardown_name = 'teardown_function' - setup_func_or_method = xunitsetup(obj, setup_name) + setup_func_or_method = _get_xunit_setup_teardown(obj, setup_name, param_obj=self.obj) if setup_func_or_method is not None: - setup_func_or_method(self.obj) - fin = getattr(obj, teardown_name, None) - if fin is not None: - self.addfinalizer(lambda: fin(self.obj)) + setup_func_or_method() + teardown_func_or_method = _get_xunit_setup_teardown(obj, teardown_name, param_obj=self.obj) + if teardown_func_or_method is not None: + self.addfinalizer(teardown_func_or_method) def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: @@ -1494,11 +1507,3 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr): fixtures.fillfixtures(self) - - - - -def xunitsetup(obj, name): - meth = getattr(obj, name, None) - if fixtures.getfixturemarker(meth) is None: - return meth diff --git a/doc/en/xunit_setup.rst b/doc/en/xunit_setup.rst index 7a80f1299..148fb1209 100644 --- a/doc/en/xunit_setup.rst +++ b/doc/en/xunit_setup.rst @@ -7,21 +7,20 @@ classic xunit-style setup This section describes a classic and popular way how you can implement fixtures (setup and teardown test state) on a per-module/class/function basis. -pytest started supporting these methods around 2005 and subsequently -nose and the standard library introduced them (under slightly different -names). While these setup/teardown methods are and will remain fully -supported you may also use pytest's more powerful :ref:`fixture mechanism -` which leverages the concept of dependency injection, allowing -for a more modular and more scalable approach for managing test state, -especially for larger projects and for functional testing. You can -mix both fixture mechanisms in the same file but unittest-based -test methods cannot receive fixture arguments. + .. note:: - As of pytest-2.4, teardownX functions are not called if - setupX existed and failed/was skipped. This harmonizes - behaviour across all major python testing tools. + While these setup/teardown methods are simple and familiar to those + coming from a ``unittest`` or nose ``background``, you may also consider + using pytest's more powerful :ref:`fixture mechanism + ` which leverages the concept of dependency injection, allowing + for a more modular and more scalable approach for managing test state, + especially for larger projects and for functional testing. You can + mix both fixture mechanisms in the same file but + test methods of ``unittest.TestCase`` subclasses + cannot receive fixture arguments. + Module level setup/teardown -------------------------------------- @@ -38,6 +37,8 @@ which will usually be called once for all the functions:: method. """ +As of pytest-3.0, the ``module`` parameter is optional. + Class level setup/teardown ---------------------------------- @@ -71,6 +72,8 @@ Similarly, the following methods are called around each method invocation:: call. """ +As of pytest-3.0, the ``method`` parameter is optional. + If you would rather define test functions directly at module level you can also use the following functions to implement fixtures:: @@ -84,7 +87,13 @@ you can also use the following functions to implement fixtures:: call. """ -Note that it is possible for setup/teardown pairs to be invoked multiple times -per testing process. +As of pytest-3.0, the ``function`` parameter is optional. + +Remarks: + +* It is possible for setup/teardown pairs to be invoked multiple times + per testing process. +* teardown functions are not called if the corresponding setup function existed + and failed/was skipped. .. _`unittest.py module`: http://docs.python.org/library/unittest.html diff --git a/testing/test_runner.py b/testing/test_runner.py index 5826d9601..bc3ff6c89 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -229,11 +229,12 @@ class BaseFunctionalTests: assert reps[5].failed def test_exact_teardown_issue1206(self, testdir): + """issue shadowing error with wrong number of arguments on teardown_method.""" rec = testdir.inline_runsource(""" import pytest class TestClass: - def teardown_method(self): + def teardown_method(self, x, y, z): pass def test_method(self): @@ -256,9 +257,9 @@ class BaseFunctionalTests: assert reps[2].when == "teardown" assert reps[2].longrepr.reprcrash.message in ( # python3 error - 'TypeError: teardown_method() takes 1 positional argument but 2 were given', + "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'", # python2 error - 'TypeError: teardown_method() takes exactly 1 argument (2 given)' + 'TypeError: teardown_method() takes exactly 4 arguments (2 given)' ) def test_failure_in_setup_function_ignores_custom_repr(self, testdir): diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index dc8ae9992..e1f0924c6 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -1,6 +1,8 @@ # # test correct setup/teardowns at # module, class, and instance level +import pytest + def test_module_and_function_setup(testdir): reprec = testdir.inline_runsource(""" @@ -251,3 +253,53 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir): "*2 error*" ]) assert "xyz43" not in result.stdout.str() + + +@pytest.mark.parametrize('arg', ['', 'arg']) +def test_setup_teardown_function_level_with_optional_argument(testdir, monkeypatch, arg): + """parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" + import sys + trace_setups_teardowns = [] + monkeypatch.setattr(sys, 'trace_setups_teardowns', trace_setups_teardowns, raising=False) + p = testdir.makepyfile(""" + import pytest + import sys + + trace = sys.trace_setups_teardowns.append + + def setup_module({arg}): trace('setup_module') + def teardown_module({arg}): trace('teardown_module') + + def setup_function({arg}): trace('setup_function') + def teardown_function({arg}): trace('teardown_function') + + def test_function_1(): pass + def test_function_2(): pass + + class Test: + def setup_method(self, {arg}): trace('setup_method') + def teardown_method(self, {arg}): trace('teardown_method') + + def test_method_1(self): pass + def test_method_2(self): pass + """.format(arg=arg)) + result = testdir.inline_run(p) + result.assertoutcome(passed=4) + + expected = [ + 'setup_module', + + 'setup_function', + 'teardown_function', + 'setup_function', + 'teardown_function', + + 'setup_method', + 'teardown_method', + + 'setup_method', + 'teardown_method', + + 'teardown_module', + ] + assert trace_setups_teardowns == expected