diff --git a/AUTHORS b/AUTHORS index 4e7c756c0..b61411874 100644 --- a/AUTHORS +++ b/AUTHORS @@ -225,6 +225,7 @@ Marcin Bachry Marco Gorelli Mark Abramowitz Mark Dickinson +Marko Pacak Markus Unterwaditzer Martijn Faassen Martin Altmayer diff --git a/changelog/10525.feature.rst b/changelog/10525.feature.rst new file mode 100644 index 000000000..15652b024 --- /dev/null +++ b/changelog/10525.feature.rst @@ -0,0 +1 @@ +Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods. diff --git a/doc/en/explanation/goodpractices.rst b/doc/en/explanation/goodpractices.rst index 6b3af861a..0e23e9e47 100644 --- a/doc/en/explanation/goodpractices.rst +++ b/doc/en/explanation/goodpractices.rst @@ -50,8 +50,8 @@ Conventions for Python test discovery * In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_. * From those files, collect test items: - * ``test`` prefixed test functions or methods outside of class - * ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method) + * ``test`` prefixed test functions or methods outside of class. + * ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered. For examples of how to customize your test discovery :doc:`/example/pythoncollection`. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1e30d42ce..e143d28d1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -403,8 +403,8 @@ class PyCollector(PyobjMixin, nodes.Collector): def istestfunction(self, obj: object, name: str) -> bool: if self.funcnamefilter(name) or self.isnosetest(obj): - if isinstance(obj, staticmethod): - # staticmethods need to be unwrapped. + if isinstance(obj, (staticmethod, classmethod)): + # staticmethods and classmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) return callable(obj) and fixtures.getfixturemarker(obj) is None else: diff --git a/testing/python/integration.py b/testing/python/integration.py index 6b5c53c98..054c14a39 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -416,7 +416,7 @@ def test_function_instance(pytester: Pytester) -> None: def test_static(): pass """ ) - assert len(items) == 3 + assert len(items) == 4 assert isinstance(items[0], Function) assert items[0].name == "test_func" assert items[0].instance is None @@ -424,6 +424,6 @@ def test_function_instance(pytester: Pytester) -> None: assert items[1].name == "test_method" assert items[1].instance is not None assert items[1].instance.__class__.__name__ == "TestIt" - assert isinstance(items[2], Function) - assert items[2].name == "test_static" - assert items[2].instance is None + assert isinstance(items[3], Function) + assert items[3].name == "test_static" + assert items[3].instance is None diff --git a/testing/test_collection.py b/testing/test_collection.py index 58e1d862a..d907244d5 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -735,6 +735,20 @@ class Test_genitems: assert s.endswith("test_example_items1.testone") print(s) + def test_classmethod_is_discovered(self, pytester: Pytester) -> None: + """Test that classmethods are discovered""" + p = pytester.makepyfile( + """ + class TestCase: + @classmethod + def test_classmethod(cls) -> None: + pass + """ + ) + items, reprec = pytester.inline_genitems(p) + ids = [x.getmodpath() for x in items] # type: ignore[attr-defined] + assert ids == ["TestCase.test_classmethod"] + def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None: """Test that Python_classes and Python_functions config options work as prefixes and glob-like patterns (#600)."""