Merge pull request #1734 from nicoddemus/issue-1728-inconsistent-setup-teardown

setup_* and teardown_* functions argument now optional
This commit is contained in:
Ronny Pfannschmidt 2016-07-15 14:59:22 +02:00 committed by GitHub
commit 8a73a2ad60
5 changed files with 129 additions and 57 deletions

View File

@ -163,6 +163,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`_).
@ -312,6 +316,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

View File

@ -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

View File

@ -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
<fixture>` 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
<fixture>` 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

View File

@ -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):

View File

@ -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