diff --git a/AUTHORS b/AUTHORS index 374e6ad9bc..b1dd40dbd4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -400,6 +400,7 @@ Stefanie Molin Stefano Taschini Steffen Allner Stephan Obermann +Sven Sven-Hendrik Haase Sviatoslav Sydorenko Sylvain MariƩ diff --git a/changelog/12749.feature.rst b/changelog/12749.feature.rst new file mode 100644 index 0000000000..a6fb8bf9e6 --- /dev/null +++ b/changelog/12749.feature.rst @@ -0,0 +1,3 @@ +Add :confval:`discover_imports`, when disabled (default) will make sure to not consider classes which are imported by a test file and starts with Test. + +-- by :user:`FreerGit` diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e5534e98d6..4887d336b2 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -78,6 +78,11 @@ def pytest_addoption(parser: Parser) -> None: type="args", default=[], ) + parser.addini( + "discover_imports", + "Whether to discover tests in imported modules outside `testpaths`", + default=False, + ) group = parser.getgroup("general", "Running and selection options") group._addoption( "-x", diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9c54dd20f8..9467b26fd0 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -741,6 +741,12 @@ def newinstance(self): return self.obj() def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + if self.config.getini("discover_imports") == ("false" or False): + paths = self.config.getini("testpaths") + class_file = inspect.getfile(self.obj) + if not any(string in class_file for string in paths): + return [] + if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): diff --git a/testing/test_discover_imports.py b/testing/test_discover_imports.py new file mode 100644 index 0000000000..cdb55916ba --- /dev/null +++ b/testing/test_discover_imports.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import textwrap + + +def test_discover_imports_enabled(pytester): + src_dir = pytester.mkdir("src") + tests_dir = pytester.mkdir("tests") + pytester.makeini(""" + [pytest] + testpaths = "tests" + discover_imports = true + """) + + src_file = src_dir / "foo.py" + + src_file.write_text( + textwrap.dedent("""\ + class TestClass(object): + def __init__(self): + super().__init__() + + def test_foobar(self): + return true + """), + encoding="utf-8", + ) + + test_file = tests_dir / "foo_test.py" + test_file.write_text( + textwrap.dedent("""\ + import sys + import os + + current_file = os.path.abspath(__file__) + current_dir = os.path.dirname(current_file) + parent_dir = os.path.abspath(os.path.join(current_dir, '..')) + sys.path.append(parent_dir) + + from src.foo import TestClass + + class TestDomain: + def test_testament(self): + testament = TestClass() + pass + """), + encoding="utf-8", + ) + + result = pytester.runpytest() + result.assert_outcomes(errors=1) + + +def test_discover_imports_disabled(pytester): + src_dir = pytester.mkdir("src") + tests_dir = pytester.mkdir("tests") + pytester.makeini(""" + [pytest] + testpaths = "tests" + discover_imports = false + """) + + src_file = src_dir / "foo.py" + + src_file.write_text( + textwrap.dedent("""\ + class Testament(object): + def __init__(self): + super().__init__() + self.collections = ["stamp", "coin"] + + def personal_property(self): + return [f"my {x} collection" for x in self.collections] + """), + encoding="utf-8", + ) + + test_file = tests_dir / "foo_test.py" + test_file.write_text( + textwrap.dedent("""\ + import sys + import os + + current_file = os.path.abspath(__file__) + current_dir = os.path.dirname(current_file) + parent_dir = os.path.abspath(os.path.join(current_dir, '..')) + sys.path.append(parent_dir) + + from src.foo import Testament + + class TestDomain: + def test_testament(self): + testament = Testament() + assert testament.personal_property() + """), + encoding="utf-8", + ) + + result = pytester.runpytest() + result.assert_outcomes(passed=1)