Skip to content

Commit

Permalink
Merge pull request #3306 from nicoddemus/2405-scope-fixture-order
Browse files Browse the repository at this point in the history
Instantiate fixtures by scope order in test function requests
  • Loading branch information
nicoddemus committed Mar 21, 2018
2 parents add5ce0 + 59e7fd4 commit 6f95189
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 17 deletions.
13 changes: 10 additions & 3 deletions _pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,9 +1021,6 @@ def _getautousenames(self, nodeid):
if nextchar and nextchar not in ":/":
continue
autousenames.extend(basenames)
# make sure autousenames are sorted by scope, scopenum 0 is session
autousenames.sort(
key=lambda x: self._arg2fixturedefs[x][-1].scopenum)
return autousenames

def getfixtureclosure(self, fixturenames, parentnode):
Expand Down Expand Up @@ -1054,6 +1051,16 @@ def merge(otherlist):
if fixturedefs:
arg2fixturedefs[argname] = fixturedefs
merge(fixturedefs[-1].argnames)

def sort_by_scope(arg_name):
try:
fixturedefs = arg2fixturedefs[arg_name]
except KeyError:
return scopes.index('function')
else:
return fixturedefs[-1].scopenum

fixturenames_closure.sort(key=sort_by_scope)
return fixturenames_closure, arg2fixturedefs

def pytest_generate_tests(self, metafunc):
Expand Down
1 change: 1 addition & 0 deletions changelog/2405.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixtures are now instantiated based on their scopes, with higher-scoped fixtures (such as ``session``) being instantiated first than lower-scoped fixtures (such as ``function``). The relative order of fixtures of the same scope is kept unchanged, based in their declaration order and their dependencies.
44 changes: 44 additions & 0 deletions doc/en/fixture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,50 @@ instance, you can simply declare it:
Finally, the ``class`` scope will invoke the fixture once per test *class*.


Higher-scoped fixtures are instantiated first
---------------------------------------------

.. versionadded:: 3.5

Within a function request for features, fixture of higher-scopes (such as ``session``) are instantiated first than
lower-scoped fixtures (such as ``function`` or ``class``). The relative order of fixtures of same scope follows
the declared order in the test function and honours dependencies between fixtures.

Consider the code below:

.. code-block:: python
@pytest.fixture(scope="session")
def s1():
pass
@pytest.fixture(scope="module")
def m1():
pass
@pytest.fixture
def f1(tmpdir):
pass
@pytest.fixture
def f2():
pass
def test_foo(f1, m1, f2, s1):
...
The fixtures requested by ``test_foo`` will be instantiated in the following order:

1. ``s1``: is the highest-scoped fixture (``session``).
2. ``m1``: is the second highest-scoped fixture (``module``).
3. ``tempdir``: is a ``function``-scoped fixture, required by ``f1``: it needs to be instantiated at this point
because it is a dependency of ``f1``.
4. ``f1``: is the first ``function``-scoped fixture in ``test_foo`` parameter list.
5. ``f2``: is the last ``function``-scoped fixture in ``test_foo`` parameter list.


.. _`finalization`:

Fixture finalization / executing teardown code
Expand Down
24 changes: 24 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,3 +964,27 @@ def test2():
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 2 passed *'])


def test_fixture_order_respects_scope(testdir):
"""Ensure that fixtures are created according to scope order, regression test for #2405
"""
testdir.makepyfile('''
import pytest
data = {}
@pytest.fixture(scope='module')
def clean_data():
data.clear()
@pytest.fixture(autouse=True)
def add_data():
data.update(value=True)
@pytest.mark.usefixtures('clean_data')
def test_value():
assert data.get('value')
''')
result = testdir.runpytest()
assert result.ret == 0
213 changes: 199 additions & 14 deletions testing/python/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import _pytest._code
import pytest
from _pytest.pytester import get_public_names
from _pytest.fixtures import FixtureLookupError
from _pytest.fixtures import FixtureLookupError, FixtureRequest
from _pytest import fixtures


Expand Down Expand Up @@ -2281,19 +2281,19 @@ def test_3(self):
pass
""")
result = testdir.runpytest("-vs")
result.stdout.fnmatch_lines("""
test_class_ordering.py::TestClass2::test_1[1-a] PASSED
test_class_ordering.py::TestClass2::test_1[2-a] PASSED
test_class_ordering.py::TestClass2::test_2[1-a] PASSED
test_class_ordering.py::TestClass2::test_2[2-a] PASSED
test_class_ordering.py::TestClass2::test_1[1-b] PASSED
test_class_ordering.py::TestClass2::test_1[2-b] PASSED
test_class_ordering.py::TestClass2::test_2[1-b] PASSED
test_class_ordering.py::TestClass2::test_2[2-b] PASSED
test_class_ordering.py::TestClass::test_3[1-a] PASSED
test_class_ordering.py::TestClass::test_3[2-a] PASSED
test_class_ordering.py::TestClass::test_3[1-b] PASSED
test_class_ordering.py::TestClass::test_3[2-b] PASSED
result.stdout.re_match_lines(r"""
test_class_ordering.py::TestClass2::test_1\[a-1\] PASSED
test_class_ordering.py::TestClass2::test_1\[a-2\] PASSED
test_class_ordering.py::TestClass2::test_2\[a-1\] PASSED
test_class_ordering.py::TestClass2::test_2\[a-2\] PASSED
test_class_ordering.py::TestClass2::test_1\[b-1\] PASSED
test_class_ordering.py::TestClass2::test_1\[b-2\] PASSED
test_class_ordering.py::TestClass2::test_2\[b-1\] PASSED
test_class_ordering.py::TestClass2::test_2\[b-2\] PASSED
test_class_ordering.py::TestClass::test_3\[a-1\] PASSED
test_class_ordering.py::TestClass::test_3\[a-2\] PASSED
test_class_ordering.py::TestClass::test_3\[b-1\] PASSED
test_class_ordering.py::TestClass::test_3\[b-2\] PASSED
""")

def test_parametrize_separated_order_higher_scope_first(self, testdir):
Expand Down Expand Up @@ -3245,3 +3245,188 @@ def test_func(my_fixture):
"*TESTS finalizer hook called for my_fixture from test_func*",
"*ROOT finalizer hook called for my_fixture from test_func*",
])


class TestScopeOrdering(object):
"""Class of tests that ensure fixtures are ordered based on their scopes (#2405)"""

@pytest.mark.parametrize('use_mark', [True, False])
def test_func_closure_module_auto(self, testdir, use_mark):
"""Semantically identical to the example posted in #2405 when ``use_mark=True``"""
testdir.makepyfile("""
import pytest
@pytest.fixture(scope='module', autouse={autouse})
def m1(): pass
if {use_mark}:
pytestmark = pytest.mark.usefixtures('m1')
@pytest.fixture(scope='function', autouse=True)
def f1(): pass
def test_func(m1):
pass
""".format(autouse=not use_mark, use_mark=use_mark))
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
assert request.fixturenames == 'm1 f1'.split()

def test_func_closure_with_native_fixtures(self, testdir, monkeypatch):
"""Sanity check that verifies the order returned by the closures and the actual fixture execution order:
The execution order may differ because of fixture inter-dependencies.
"""
monkeypatch.setattr(pytest, 'FIXTURE_ORDER', [], raising=False)
testdir.makepyfile("""
import pytest
FIXTURE_ORDER = pytest.FIXTURE_ORDER
@pytest.fixture(scope="session")
def s1():
FIXTURE_ORDER.append('s1')
@pytest.fixture(scope="module")
def m1():
FIXTURE_ORDER.append('m1')
@pytest.fixture(scope='session')
def my_tmpdir_factory():
FIXTURE_ORDER.append('my_tmpdir_factory')
@pytest.fixture
def my_tmpdir(my_tmpdir_factory):
FIXTURE_ORDER.append('my_tmpdir')
@pytest.fixture
def f1(my_tmpdir):
FIXTURE_ORDER.append('f1')
@pytest.fixture
def f2():
FIXTURE_ORDER.append('f2')
def test_foo(f1, m1, f2, s1): pass
""")
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
# order of fixtures based on their scope and position in the parameter list
assert request.fixturenames == 's1 my_tmpdir_factory m1 f1 f2 my_tmpdir'.split()
testdir.runpytest()
# actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir")
assert pytest.FIXTURE_ORDER == 's1 my_tmpdir_factory m1 my_tmpdir f1 f2'.split()

def test_func_closure_module(self, testdir):
testdir.makepyfile("""
import pytest
@pytest.fixture(scope='module')
def m1(): pass
@pytest.fixture(scope='function')
def f1(): pass
def test_func(f1, m1):
pass
""")
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
assert request.fixturenames == 'm1 f1'.split()

def test_func_closure_scopes_reordered(self, testdir):
"""Test ensures that fixtures are ordered by scope regardless of the order of the parameters, although
fixtures of same scope keep the declared order
"""
testdir.makepyfile("""
import pytest
@pytest.fixture(scope='session')
def s1(): pass
@pytest.fixture(scope='module')
def m1(): pass
@pytest.fixture(scope='function')
def f1(): pass
@pytest.fixture(scope='function')
def f2(): pass
class Test:
@pytest.fixture(scope='class')
def c1(cls): pass
def test_func(self, f2, f1, c1, m1, s1):
pass
""")
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
assert request.fixturenames == 's1 m1 c1 f2 f1'.split()

def test_func_closure_same_scope_closer_root_first(self, testdir):
"""Auto-use fixtures of same scope are ordered by closer-to-root first"""
testdir.makeconftest("""
import pytest
@pytest.fixture(scope='module', autouse=True)
def m_conf(): pass
""")
testdir.makepyfile(**{
'sub/conftest.py': """
import pytest
@pytest.fixture(scope='module', autouse=True)
def m_sub(): pass
""",
'sub/test_func.py': """
import pytest
@pytest.fixture(scope='module', autouse=True)
def m_test(): pass
@pytest.fixture(scope='function')
def f1(): pass
def test_func(m_test, f1):
pass
"""})
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
assert request.fixturenames == 'm_conf m_sub m_test f1'.split()

def test_func_closure_all_scopes_complex(self, testdir):
"""Complex test involving all scopes and mixing autouse with normal fixtures"""
testdir.makeconftest("""
import pytest
@pytest.fixture(scope='session')
def s1(): pass
""")
testdir.makepyfile("""
import pytest
@pytest.fixture(scope='module', autouse=True)
def m1(): pass
@pytest.fixture(scope='module')
def m2(s1): pass
@pytest.fixture(scope='function')
def f1(): pass
@pytest.fixture(scope='function')
def f2(): pass
class Test:
@pytest.fixture(scope='class', autouse=True)
def c1(self):
pass
def test_func(self, f2, f1, m2):
pass
""")
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
assert request.fixturenames == 's1 m1 m2 c1 f2 f1'.split()

0 comments on commit 6f95189

Please sign in to comment.