Merge pull request #1734 from nicoddemus/issue-1728-inconsistent-setup-teardown
setup_* and teardown_* functions argument now optional
This commit is contained in:
commit
8a73a2ad60
|
@ -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.
|
automatically generated id for that argument will be used.
|
||||||
Thanks `@palaviv`_ for the complete PR (`#1468`_).
|
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
|
* Improved automatic id generation selection in case of duplicate ids in
|
||||||
parametrize.
|
parametrize.
|
||||||
Thanks `@palaviv`_ for the complete PR (`#1474`_).
|
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
|
.. _@nikratio: https://github.com/nikratio
|
||||||
.. _@novas0x2a: https://github.com/novas0x2a
|
.. _@novas0x2a: https://github.com/novas0x2a
|
||||||
.. _@obestwalter: https://github.com/obestwalter
|
.. _@obestwalter: https://github.com/obestwalter
|
||||||
|
.. _@okken: https://github.com/okken
|
||||||
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
|
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
|
||||||
.. _@omarkohl: https://github.com/omarkohl
|
.. _@omarkohl: https://github.com/omarkohl
|
||||||
.. _@palaviv: https://github.com/palaviv
|
.. _@palaviv: https://github.com/palaviv
|
||||||
|
|
|
@ -25,10 +25,6 @@ cutdir2 = py.path.local(_pytest.__file__).dirpath()
|
||||||
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||||
|
|
||||||
|
|
||||||
def _has_positional_arg(func):
|
|
||||||
return func.__code__.co_argcount
|
|
||||||
|
|
||||||
|
|
||||||
def filter_traceback(entry):
|
def filter_traceback(entry):
|
||||||
# entry.path might sometimes return a str object when the entry
|
# entry.path might sometimes return a str object when the entry
|
||||||
# points to dynamically generated code
|
# points to dynamically generated code
|
||||||
|
@ -439,34 +435,51 @@ class Module(pytest.File, PyCollector):
|
||||||
"decorator) is not allowed. Use @pytest.mark.skip or "
|
"decorator) is not allowed. Use @pytest.mark.skip or "
|
||||||
"@pytest.mark.skipif instead."
|
"@pytest.mark.skipif instead."
|
||||||
)
|
)
|
||||||
#print "imported test module", mod
|
|
||||||
self.config.pluginmanager.consider_module(mod)
|
self.config.pluginmanager.consider_module(mod)
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
setup_module = xunitsetup(self.obj, "setUpModule")
|
setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule")
|
||||||
if setup_module is None:
|
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:
|
if setup_module is not None:
|
||||||
#XXX: nose compat hack, move to nose plugin
|
setup_module()
|
||||||
# if it takes a positional arg, its probably a pytest style one
|
|
||||||
# so we pass the current module object
|
teardown_module = _get_xunit_setup_teardown(self.obj, 'tearDownModule')
|
||||||
if _has_positional_arg(setup_module):
|
if teardown_module is None:
|
||||||
setup_module(self.obj)
|
teardown_module = _get_xunit_setup_teardown(self.obj, 'teardown_module')
|
||||||
else:
|
if teardown_module is not None:
|
||||||
setup_module()
|
self.addfinalizer(teardown_module)
|
||||||
fin = getattr(self.obj, 'tearDownModule', None)
|
|
||||||
if fin is None:
|
|
||||||
fin = getattr(self.obj, 'teardown_module', None)
|
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
|
||||||
if fin is not None:
|
"""
|
||||||
#XXX: nose compat hack, move to nose plugin
|
Return a callable to perform xunit-style setup or teardown if
|
||||||
# if it takes a positional arg, it's probably a pytest style one
|
the function exists in the ``holder`` object.
|
||||||
# so we pass the current module object
|
The ``param_obj`` parameter is the parameter which will be passed to the function
|
||||||
if _has_positional_arg(fin):
|
when the callable is called without arguments, defaults to the ``holder`` object.
|
||||||
finalizer = lambda: fin(self.obj)
|
Return ``None`` if a suitable callable is not found.
|
||||||
else:
|
"""
|
||||||
finalizer = fin
|
param_obj = param_obj if param_obj is not None else holder
|
||||||
self.addfinalizer(finalizer)
|
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):
|
class Class(PyCollector):
|
||||||
|
@ -479,7 +492,7 @@ class Class(PyCollector):
|
||||||
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
||||||
|
|
||||||
def setup(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:
|
if setup_class is not None:
|
||||||
setup_class = getattr(setup_class, 'im_func', setup_class)
|
setup_class = getattr(setup_class, 'im_func', setup_class)
|
||||||
setup_class = getattr(setup_class, '__func__', setup_class)
|
setup_class = getattr(setup_class, '__func__', setup_class)
|
||||||
|
@ -523,12 +536,12 @@ class FunctionMixin(PyobjMixin):
|
||||||
else:
|
else:
|
||||||
setup_name = 'setup_function'
|
setup_name = 'setup_function'
|
||||||
teardown_name = 'teardown_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:
|
if setup_func_or_method is not None:
|
||||||
setup_func_or_method(self.obj)
|
setup_func_or_method()
|
||||||
fin = getattr(obj, teardown_name, None)
|
teardown_func_or_method = _get_xunit_setup_teardown(obj, teardown_name, param_obj=self.obj)
|
||||||
if fin is not None:
|
if teardown_func_or_method is not None:
|
||||||
self.addfinalizer(lambda: fin(self.obj))
|
self.addfinalizer(teardown_func_or_method)
|
||||||
|
|
||||||
def _prunetraceback(self, excinfo):
|
def _prunetraceback(self, excinfo):
|
||||||
if hasattr(self, '_obj') and not self.config.option.fulltrace:
|
if hasattr(self, '_obj') and not self.config.option.fulltrace:
|
||||||
|
@ -1494,11 +1507,3 @@ class Function(FunctionMixin, pytest.Item, fixtures.FuncargnamesCompatAttr):
|
||||||
fixtures.fillfixtures(self)
|
fixtures.fillfixtures(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def xunitsetup(obj, name):
|
|
||||||
meth = getattr(obj, name, None)
|
|
||||||
if fixtures.getfixturemarker(meth) is None:
|
|
||||||
return meth
|
|
||||||
|
|
|
@ -7,21 +7,20 @@ classic xunit-style setup
|
||||||
|
|
||||||
This section describes a classic and popular way how you can implement
|
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.
|
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::
|
.. note::
|
||||||
|
|
||||||
As of pytest-2.4, teardownX functions are not called if
|
While these setup/teardown methods are simple and familiar to those
|
||||||
setupX existed and failed/was skipped. This harmonizes
|
coming from a ``unittest`` or nose ``background``, you may also consider
|
||||||
behaviour across all major python testing tools.
|
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
|
Module level setup/teardown
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
@ -38,6 +37,8 @@ which will usually be called once for all the functions::
|
||||||
method.
|
method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
As of pytest-3.0, the ``module`` parameter is optional.
|
||||||
|
|
||||||
Class level setup/teardown
|
Class level setup/teardown
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
@ -71,6 +72,8 @@ Similarly, the following methods are called around each method invocation::
|
||||||
call.
|
call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
As of pytest-3.0, the ``method`` parameter is optional.
|
||||||
|
|
||||||
If you would rather define test functions directly at module level
|
If you would rather define test functions directly at module level
|
||||||
you can also use the following functions to implement fixtures::
|
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.
|
call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Note that it is possible for setup/teardown pairs to be invoked multiple times
|
As of pytest-3.0, the ``function`` parameter is optional.
|
||||||
per testing process.
|
|
||||||
|
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
|
.. _`unittest.py module`: http://docs.python.org/library/unittest.html
|
||||||
|
|
|
@ -229,11 +229,12 @@ class BaseFunctionalTests:
|
||||||
assert reps[5].failed
|
assert reps[5].failed
|
||||||
|
|
||||||
def test_exact_teardown_issue1206(self, testdir):
|
def test_exact_teardown_issue1206(self, testdir):
|
||||||
|
"""issue shadowing error with wrong number of arguments on teardown_method."""
|
||||||
rec = testdir.inline_runsource("""
|
rec = testdir.inline_runsource("""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
class TestClass:
|
class TestClass:
|
||||||
def teardown_method(self):
|
def teardown_method(self, x, y, z):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_method(self):
|
def test_method(self):
|
||||||
|
@ -256,9 +257,9 @@ class BaseFunctionalTests:
|
||||||
assert reps[2].when == "teardown"
|
assert reps[2].when == "teardown"
|
||||||
assert reps[2].longrepr.reprcrash.message in (
|
assert reps[2].longrepr.reprcrash.message in (
|
||||||
# python3 error
|
# 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
|
# 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):
|
def test_failure_in_setup_function_ignores_custom_repr(self, testdir):
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#
|
#
|
||||||
# test correct setup/teardowns at
|
# test correct setup/teardowns at
|
||||||
# module, class, and instance level
|
# module, class, and instance level
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_module_and_function_setup(testdir):
|
def test_module_and_function_setup(testdir):
|
||||||
reprec = testdir.inline_runsource("""
|
reprec = testdir.inline_runsource("""
|
||||||
|
@ -251,3 +253,53 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
|
||||||
"*2 error*"
|
"*2 error*"
|
||||||
])
|
])
|
||||||
assert "xyz43" not in result.stdout.str()
|
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
|
||||||
|
|
Loading…
Reference in New Issue