Skip to content

setup_* and teardown_* functions argument now optional #1734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`_).
Expand Down Expand Up @@ -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
Expand Down
85 changes: 45 additions & 40 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@
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
Expand Down Expand Up @@ -439,34 +435,51 @@ def _importtestmodule(self):
"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):
Expand All @@ -479,7 +492,7 @@ def collect(self):
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)
Expand Down Expand Up @@ -523,12 +536,12 @@ def setup(self):
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:
Expand Down Expand Up @@ -1494,11 +1507,3 @@ def setup(self):
fixtures.fillfixtures(self)






def xunitsetup(obj, name):
meth = getattr(obj, name, None)
if fixtures.getfixturemarker(meth) is None:
return meth
37 changes: 23 additions & 14 deletions doc/en/xunit_setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------------------
Expand All @@ -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
----------------------------------

Expand Down Expand Up @@ -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::

Expand All @@ -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
7 changes: 4 additions & 3 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,12 @@ def teardown_function(func):
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):
Expand All @@ -256,9 +257,9 @@ def test_method(self):
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):
Expand Down
52 changes: 52 additions & 0 deletions testing/test_runner_xunit.py
Original file line number Diff line number Diff line change
@@ -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("""
Expand Down Expand Up @@ -251,3 +253,53 @@ def test_function2(hello):
"*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