3
# tests/test_assert.py
@pytest.mark.mymark
def custom_assert():
    assert True 

How do I force pytest to discover this test?

In general, how do I dynamically add any test to pytest's list of discovered tests, even if they don't fit in the naming convention?

Some Guy
  • 576
  • 1
  • 4
  • 17
  • Does this answer your question? [Pytest - no tests ran](https://stackoverflow.com/questions/34363388) – Jorge Luis Mar 08 '23 at 09:11
  • No. I know about the naming convention. I want to add this test to the discovered tests without renaming it. – Some Guy Mar 08 '23 at 09:22

2 Answers2

3

pytest is fairly customisable, but you'll have to look at its extensive API. Luckily, the code base is statically typed, so you can navigate from functions and classes to other functions and classes fairly easily.

To start off, it pays to understand how pytest discovers tests. Recall the configurable discovery naming conventions:

# content of pytest.ini
# Example 1: have pytest look for "check" instead of "test"
[pytest]
python_files = check_*.py
python_classes = Check
python_functions = *_check

This implies that, for example, the value to python_functions is used somewhere to filter out functions that are not considered as test functions. Do a quick search on the pytest repository to see this:

class PyCollector(PyobjMixin, nodes.Collector):
    def funcnamefilter(self, name: str) -> bool:
        return self._matches_prefix_or_glob_option("python_functions", name)

PyCollector is a base class for pytest Module objects, and module_: pytest.Module has an obj property which is the types.ModuleType object itself. Along with access to the funcnamefilter::name parameter, you can make a subclass of pytest.Module, pytest.Package, and pytest.Class to override funcnamefilter to accept functions decorated your custom @pytest.mark.mymark decorator as test functions:

from __future__ import annotations

import types
import typing as t

import pytest


# Static-type-friendliness
if t.TYPE_CHECKING:
    from _pytest.python import PyCollector

    class _MarkDecorated(t.Protocol):
        pytestmark: list[pytest.Mark]

        def __call__(self, *args: object, **kwargs: object) -> None:
            """Test function callback method"""

else:
    PyCollector: t.TypeAlias = object


def _isPytestMarkDecorated(obj: object) -> t.TypeGuard[_MarkDecorated]:

    """
    Decorating `@pytest.mark.mymark` over a function results in this:

    >>> @pytest.mark.mymark
    ... def f() -> None:
    ...     pass
    ...
    >>> f.pytestmark
    [Mark(name='mymark', args=(), kwargs={})]

    where `Mark` is `pytest.Mark`.

    This function provides a type guard for static typing purposes.
    """

    if (
        callable(obj)
        and hasattr(obj, "pytestmark")
        and isinstance(obj.pytestmark, list)
    ):
        return True
    return False

class _MyMarkMixin(PyCollector):
    def funcnamefilter(self, name: str) -> bool:
        underlying_py_obj: object = self.obj
        assert isinstance(underlying_py_obj, (types.ModuleType, type))
        func: object = getattr(underlying_py_obj, name)
        if _isPytestMarkDecorated(func) and any(
            mark.name == "mymark" for mark in func.pytestmark
        ):
            return True
        return super().funcnamefilter(name)


class MyMarkModule(_MyMarkMixin, pytest.Module):
    pass

The last thing to do is to configure pytest to use your MyMarkModule rather than pytest.Module when collecting test modules. You can do this with the per-directory plugin module file conftest.py, where you would override the hook pytest.pycollect.makemodule (please see pytest's implementation on how to write this properly):

# conftest.py
import typing as t
from <...> import MyMarkModule

if t.TYPE_CHECKING:
    import pathlib
    import pytest


def pytest_pycollect_makemodule(
    module_path: pathlib.Path, parent: object
) -> pytest.Module | None:
    if module_path.name != "__init__.py":
        return MyMarkModule.from_parent(parent, path=module_path)  # type: ignore[no-any-return]

Now you can run pytest <your test file> and you should see all @pytest.mark.mymark functions run as test functions, regardless of whether they're named according to the pytest_functions configuration setting.


This is a start on what you need to do, and can do with pytest. You'll have to do this with pytest.Class and pytest.Package as well, if you're planning on using @pytest.mark.mymark elsewhere.

dROOOze
  • 1,727
  • 1
  • 9
  • 17
1

Change naming convention

Pytest > Changing naming conventions

You can configure different naming conventions by setting the python_files, python_classes and python_functions in your configuration file. Here is an example:

# content of pytest.ini
# Example 1: have pytest look for "check" instead of "test"
[pytest]
python_files = check_*.py 
python_classes = Check 
python_functions = *_check

This would make pytest look for tests in files that match the check_* .py glob-pattern, Check prefixes in classes, and functions and methods that match *_check. You can check for multiple glob patterns by adding a space between the patterns.

So, in your case, you need this configuration in pytest.ini:

# content of pytest.ini
[pytest]
python_functions = *_assert
Dorian Turba
  • 3,260
  • 3
  • 23
  • 67
  • This only works for a single function. What if I have a hundred methods all named differently that I can't change? – Some Guy Mar 08 '23 at 09:20
  • 1
    "You can check for multiple glob patterns by adding a space between the patterns." add names in the python_functions config like: `what_foo example_bar assert_* *_assert spam_eggs`. It's boilerplate, but it's how you force pytest to discover those functions. – Dorian Turba Mar 08 '23 at 09:25
  • What if the method is inside a closure? What if I'm generating the method dynamically? Editing the config file is fine if you are sticking to another naming convention, not for adding specific methods in general. – Some Guy Mar 08 '23 at 09:48
  • "What if the method is inside a closure" How do you expect anything to run a function defined in a function? IMO, you are looking for a miracle, sorry. – Dorian Turba Mar 08 '23 at 14:55
  • A function defined inside a function is still an object, there's no reason why I shouldn't be able to manipulate it further. For example, the ward framework allows to append arbitrary functions to the collected tests, even ones inside closures. – Some Guy Mar 09 '23 at 17:11
  • @SomeGuy Can you provide an example, please? – Dorian Turba Mar 09 '23 at 17:42