feat(fixtures-per-test): exclude pseudo fixtures from output
Addresses issue #11295 by excluding from the --fixtures-per-test output any 'pseudo fixture' that results from directly parametrizating a test with ``@pytest.mark.parametrize``. The justification for removing these fixtures from the report is that a) They are unintuitive. Their appearance in the fixtures-per-test report confuses new users because the fixtures created via ``@pytest.mark.parametrize`` do not confrom to the expectations established in the documentation; namely, that fixtures are - richly reusable - provide setup/teardown features - created via the ``@pytest.fixture` decorator b) They are an internal implementation detail. It is not the explicit goal of the direct parametrization mark to create a fixture; instead, pytest's internals leverages the fixture system to achieve the explicit goal: a succinct batch execution syntax. Consequently, exposing the fixtures that implement the batch execution behaviour reveal more about pytest's internals than they do about the user's own design choices and test dependencies.
This commit is contained in:
parent
2fdee7b062
commit
b9e7493f8a
|
@ -1534,6 +1534,48 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
|||
return bestrelpath(invocation_dir, loc)
|
||||
|
||||
|
||||
def _get_fixtures_per_test(test: nodes.Item) -> Optional[List[FixtureDef[object]]]:
|
||||
"""Returns all fixtures used by the test item except for a) those
|
||||
created by direct parametrization with ``@pytest.mark.parametrize`` and
|
||||
b) those accessed dynamically with ``request.getfixturevalue``.
|
||||
|
||||
The justification for excluding fixtures created by direct
|
||||
parametrization is that their appearance in a report would surprise
|
||||
users currently learning about fixtures, as they do not conform to the
|
||||
documented characteristics of fixtures (reusable, providing
|
||||
setup/teardown features, and created via the ``@pytest.fixture``
|
||||
decorator).
|
||||
|
||||
In other words, an internal detail that leverages the fixture system
|
||||
to batch execute tests should not be exposed in a report that identifies
|
||||
which fixtures a user is using in their tests.
|
||||
"""
|
||||
# Contains information on the fixtures the test item requests
|
||||
# statically, if any.
|
||||
fixture_info: Optional[FuncFixtureInfo] = getattr(test, "_fixtureinfo", None)
|
||||
if fixture_info is None:
|
||||
# The given test item does not statically request any fixtures.
|
||||
return []
|
||||
|
||||
# In the transitive closure of fixture names required by the item
|
||||
# through autouse, function parameter or @userfixture mechanisms,
|
||||
# multiple overrides may have occured; for this reason, the fixture
|
||||
# names are matched to a sequence of FixtureDefs.
|
||||
name2fixturedefs = fixture_info.name2fixturedefs
|
||||
fixturedefs = [
|
||||
# The final override, which is the one the test item will utilise,
|
||||
# is stored in the final position of the sequence; therefore, we
|
||||
# take the FixtureDef of the final override and add it to the list.
|
||||
#
|
||||
# If there wasn't an ovveride, the final item will simply be the
|
||||
# first item, as required.
|
||||
fixturedefs[-1]
|
||||
for argname, fixturedefs in sorted(name2fixturedefs.items())
|
||||
if argname not in _get_direct_parametrize_args(test)
|
||||
]
|
||||
return fixturedefs
|
||||
|
||||
|
||||
def show_fixtures_per_test(config):
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
|
@ -1552,7 +1594,21 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
|||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
fixturedefs = _get_fixtures_per_test(item)
|
||||
if not fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
# Do not write anything.
|
||||
return
|
||||
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
|
||||
for fixturedef in fixturedefs:
|
||||
write_fixture(fixturedef)
|
||||
|
||||
def write_fixture(fixture_def: FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
|
@ -1568,24 +1624,6 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
|||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
# Last item is expected to be the one used by the test item.
|
||||
write_fixture(fixturedefs[-1])
|
||||
|
||||
for session_item in session.items:
|
||||
write_item(session_item)
|
||||
|
||||
|
|
Loading…
Reference in New Issue