Skip to content

Commit 8a73a2a

Browse files
Merge pull request #1734 from nicoddemus/issue-1728-inconsistent-setup-teardown
setup_* and teardown_* functions argument now optional
2 parents fb21493 + ff8fb49 commit 8a73a2a

File tree

5 files changed

+129
-57
lines changed

5 files changed

+129
-57
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ time or change existing behaviors in order to make them less surprising/more use
163163
automatically generated id for that argument will be used.
164164
Thanks `@palaviv`_ for the complete PR (`#1468`_).
165165

166+
* The parameter to xunit-style setup/teardown methods (``setup_method``,
167+
``setup_module``, etc.) is now optional and may be omitted.
168+
Thanks `@okken`_ for bringing this to attention and `@nicoddemus`_ for the PR.
169+
166170
* Improved automatic id generation selection in case of duplicate ids in
167171
parametrize.
168172
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
312316
.. _@nikratio: https://github.com/nikratio
313317
.. _@novas0x2a: https://github.com/novas0x2a
314318
.. _@obestwalter: https://github.com/obestwalter
319+
.. _@okken: https://github.com/okken
315320
.. _@olegpidsadnyi: https://github.com/olegpidsadnyi
316321
.. _@omarkohl: https://github.com/omarkohl
317322
.. _@palaviv: https://github.com/palaviv

_pytest/python.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
2626

2727

28-
def _has_positional_arg(func):
29-
return func.__code__.co_argcount
30-
31-
3228
def filter_traceback(entry):
3329
# entry.path might sometimes return a str object when the entry
3430
# points to dynamically generated code
@@ -439,34 +435,51 @@ def _importtestmodule(self):
439435
"decorator) is not allowed. Use @pytest.mark.skip or "
440436
"@pytest.mark.skipif instead."
441437
)
442-
#print "imported test module", mod
443438
self.config.pluginmanager.consider_module(mod)
444439
return mod
445440

446441
def setup(self):
447-
setup_module = xunitsetup(self.obj, "setUpModule")
442+
setup_module = _get_xunit_setup_teardown(self.obj, "setUpModule")
448443
if setup_module is None:
449-
setup_module = xunitsetup(self.obj, "setup_module")
444+
setup_module = _get_xunit_setup_teardown(self.obj, "setup_module")
450445
if setup_module is not None:
451-
#XXX: nose compat hack, move to nose plugin
452-
# if it takes a positional arg, its probably a pytest style one
453-
# so we pass the current module object
454-
if _has_positional_arg(setup_module):
455-
setup_module(self.obj)
456-
else:
457-
setup_module()
458-
fin = getattr(self.obj, 'tearDownModule', None)
459-
if fin is None:
460-
fin = getattr(self.obj, 'teardown_module', None)
461-
if fin is not None:
462-
#XXX: nose compat hack, move to nose plugin
463-
# if it takes a positional arg, it's probably a pytest style one
464-
# so we pass the current module object
465-
if _has_positional_arg(fin):
466-
finalizer = lambda: fin(self.obj)
467-
else:
468-
finalizer = fin
469-
self.addfinalizer(finalizer)
446+
setup_module()
447+
448+
teardown_module = _get_xunit_setup_teardown(self.obj, 'tearDownModule')
449+
if teardown_module is None:
450+
teardown_module = _get_xunit_setup_teardown(self.obj, 'teardown_module')
451+
if teardown_module is not None:
452+
self.addfinalizer(teardown_module)
453+
454+
455+
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
456+
"""
457+
Return a callable to perform xunit-style setup or teardown if
458+
the function exists in the ``holder`` object.
459+
The ``param_obj`` parameter is the parameter which will be passed to the function
460+
when the callable is called without arguments, defaults to the ``holder`` object.
461+
Return ``None`` if a suitable callable is not found.
462+
"""
463+
param_obj = param_obj if param_obj is not None else holder
464+
result = _get_xunit_func(holder, attr_name)
465+
if result is not None:
466+
arg_count = result.__code__.co_argcount
467+
if inspect.ismethod(result):
468+
arg_count -= 1
469+
if arg_count:
470+
return lambda: result(param_obj)
471+
else:
472+
return result
473+
474+
475+
def _get_xunit_func(obj, name):
476+
"""Return the attribute from the given object to be used as a setup/teardown
477+
xunit-style function, but only if not marked as a fixture to
478+
avoid calling it twice.
479+
"""
480+
meth = getattr(obj, name, None)
481+
if fixtures.getfixturemarker(meth) is None:
482+
return meth
470483

471484

472485
class Class(PyCollector):
@@ -479,7 +492,7 @@ def collect(self):
479492
return [self._getcustomclass("Instance")(name="()", parent=self)]
480493

481494
def setup(self):
482-
setup_class = xunitsetup(self.obj, 'setup_class')
495+
setup_class = _get_xunit_func(self.obj, 'setup_class')
483496
if setup_class is not None:
484497
setup_class = getattr(setup_class, 'im_func', setup_class)
485498
setup_class = getattr(setup_class, '__func__', setup_class)
@@ -523,12 +536,12 @@ def setup(self):
523536
else:
524537
setup_name = 'setup_function'
525538
teardown_name = 'teardown_function'
526-
setup_func_or_method = xunitsetup(obj, setup_name)
539+
setup_func_or_method = _get_xunit_setup_teardown(obj, setup_name, param_obj=self.obj)
527540
if setup_func_or_method is not None:
528-
setup_func_or_method(self.obj)
529-
fin = getattr(obj, teardown_name, None)
530-
if fin is not None:
531-
self.addfinalizer(lambda: fin(self.obj))
541+
setup_func_or_method()
542+
teardown_func_or_method = _get_xunit_setup_teardown(obj, teardown_name, param_obj=self.obj)
543+
if teardown_func_or_method is not None:
544+
self.addfinalizer(teardown_func_or_method)
532545

533546
def _prunetraceback(self, excinfo):
534547
if hasattr(self, '_obj') and not self.config.option.fulltrace:
@@ -1494,11 +1507,3 @@ def setup(self):
14941507
fixtures.fillfixtures(self)
14951508

14961509

1497-
1498-
1499-
1500-
1501-
def xunitsetup(obj, name):
1502-
meth = getattr(obj, name, None)
1503-
if fixtures.getfixturemarker(meth) is None:
1504-
return meth

doc/en/xunit_setup.rst

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,20 @@ classic xunit-style setup
77

88
This section describes a classic and popular way how you can implement
99
fixtures (setup and teardown test state) on a per-module/class/function basis.
10-
pytest started supporting these methods around 2005 and subsequently
11-
nose and the standard library introduced them (under slightly different
12-
names). While these setup/teardown methods are and will remain fully
13-
supported you may also use pytest's more powerful :ref:`fixture mechanism
14-
<fixture>` which leverages the concept of dependency injection, allowing
15-
for a more modular and more scalable approach for managing test state,
16-
especially for larger projects and for functional testing. You can
17-
mix both fixture mechanisms in the same file but unittest-based
18-
test methods cannot receive fixture arguments.
10+
1911

2012
.. note::
2113

22-
As of pytest-2.4, teardownX functions are not called if
23-
setupX existed and failed/was skipped. This harmonizes
24-
behaviour across all major python testing tools.
14+
While these setup/teardown methods are simple and familiar to those
15+
coming from a ``unittest`` or nose ``background``, you may also consider
16+
using pytest's more powerful :ref:`fixture mechanism
17+
<fixture>` which leverages the concept of dependency injection, allowing
18+
for a more modular and more scalable approach for managing test state,
19+
especially for larger projects and for functional testing. You can
20+
mix both fixture mechanisms in the same file but
21+
test methods of ``unittest.TestCase`` subclasses
22+
cannot receive fixture arguments.
23+
2524

2625
Module level setup/teardown
2726
--------------------------------------
@@ -38,6 +37,8 @@ which will usually be called once for all the functions::
3837
method.
3938
"""
4039

40+
As of pytest-3.0, the ``module`` parameter is optional.
41+
4142
Class level setup/teardown
4243
----------------------------------
4344

@@ -71,6 +72,8 @@ Similarly, the following methods are called around each method invocation::
7172
call.
7273
"""
7374

75+
As of pytest-3.0, the ``method`` parameter is optional.
76+
7477
If you would rather define test functions directly at module level
7578
you can also use the following functions to implement fixtures::
7679

@@ -84,7 +87,13 @@ you can also use the following functions to implement fixtures::
8487
call.
8588
"""
8689

87-
Note that it is possible for setup/teardown pairs to be invoked multiple times
88-
per testing process.
90+
As of pytest-3.0, the ``function`` parameter is optional.
91+
92+
Remarks:
93+
94+
* It is possible for setup/teardown pairs to be invoked multiple times
95+
per testing process.
96+
* teardown functions are not called if the corresponding setup function existed
97+
and failed/was skipped.
8998

9099
.. _`unittest.py module`: http://docs.python.org/library/unittest.html

testing/test_runner.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,12 @@ def teardown_function(func):
229229
assert reps[5].failed
230230

231231
def test_exact_teardown_issue1206(self, testdir):
232+
"""issue shadowing error with wrong number of arguments on teardown_method."""
232233
rec = testdir.inline_runsource("""
233234
import pytest
234235
235236
class TestClass:
236-
def teardown_method(self):
237+
def teardown_method(self, x, y, z):
237238
pass
238239
239240
def test_method(self):
@@ -256,9 +257,9 @@ def test_method(self):
256257
assert reps[2].when == "teardown"
257258
assert reps[2].longrepr.reprcrash.message in (
258259
# python3 error
259-
'TypeError: teardown_method() takes 1 positional argument but 2 were given',
260+
"TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'",
260261
# python2 error
261-
'TypeError: teardown_method() takes exactly 1 argument (2 given)'
262+
'TypeError: teardown_method() takes exactly 4 arguments (2 given)'
262263
)
263264

264265
def test_failure_in_setup_function_ignores_custom_repr(self, testdir):

testing/test_runner_xunit.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#
22
# test correct setup/teardowns at
33
# module, class, and instance level
4+
import pytest
5+
46

57
def test_module_and_function_setup(testdir):
68
reprec = testdir.inline_runsource("""
@@ -251,3 +253,53 @@ def test_function2(hello):
251253
"*2 error*"
252254
])
253255
assert "xyz43" not in result.stdout.str()
256+
257+
258+
@pytest.mark.parametrize('arg', ['', 'arg'])
259+
def test_setup_teardown_function_level_with_optional_argument(testdir, monkeypatch, arg):
260+
"""parameter to setup/teardown xunit-style functions parameter is now optional (#1728)."""
261+
import sys
262+
trace_setups_teardowns = []
263+
monkeypatch.setattr(sys, 'trace_setups_teardowns', trace_setups_teardowns, raising=False)
264+
p = testdir.makepyfile("""
265+
import pytest
266+
import sys
267+
268+
trace = sys.trace_setups_teardowns.append
269+
270+
def setup_module({arg}): trace('setup_module')
271+
def teardown_module({arg}): trace('teardown_module')
272+
273+
def setup_function({arg}): trace('setup_function')
274+
def teardown_function({arg}): trace('teardown_function')
275+
276+
def test_function_1(): pass
277+
def test_function_2(): pass
278+
279+
class Test:
280+
def setup_method(self, {arg}): trace('setup_method')
281+
def teardown_method(self, {arg}): trace('teardown_method')
282+
283+
def test_method_1(self): pass
284+
def test_method_2(self): pass
285+
""".format(arg=arg))
286+
result = testdir.inline_run(p)
287+
result.assertoutcome(passed=4)
288+
289+
expected = [
290+
'setup_module',
291+
292+
'setup_function',
293+
'teardown_function',
294+
'setup_function',
295+
'teardown_function',
296+
297+
'setup_method',
298+
'teardown_method',
299+
300+
'setup_method',
301+
'teardown_method',
302+
303+
'teardown_module',
304+
]
305+
assert trace_setups_teardowns == expected

0 commit comments

Comments
 (0)