Skip to content
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

Instantiate fixtures by scope order in test function requests #3306

Merged
merged 2 commits into from
Mar 21, 2018
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
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()