pytest文檔82 – 用例收集鉤子 pytest_collect_file 的使用

前言

pytest 提供了一個收集用例的鉤子,在用例收集階段,默認會查找test_*.py 文件或者 *_test.py文件。
如果我們想運行一個非python的文件,比如用yaml 文件寫用例,那麼就需要改變用例的收集規則。
以最新版pytest 7.2.0版本為例

YAML 測試示例

在 Yaml 文件中指定測試的基本示例, 以下是官方文檔上給的一個執行yaml格式的內容作為自定義測試的例子。
相關文檔地址//docs.pytest.org/en/latest/example/nonpython.html
寫到conftest.py

# content of conftest.py
import pytest


def pytest_collect_file(parent, file_path):
    if file_path.suffix == ".yaml" and file_path.name.startswith("test"):
        return YamlFile.from_parent(parent, path=file_path)


class YamlFile(pytest.File):
    def collect(self):
        # We need a yaml parser, e.g. PyYAML.
        import yaml

        raw = yaml.safe_load(self.path.open())
        for name, spec in sorted(raw.items()):
            yield YamlItem.from_parent(self, name=name, spec=spec)


class YamlItem(pytest.Item):
    def __init__(self, *, spec, **kwargs):
        super().__init__(**kwargs)
        self.spec = spec

    def runtest(self):
        for name, value in sorted(self.spec.items()):
            # Some custom test execution (dumb example follows).
            if name != value:
                raise YamlException(self, name, value)

    def repr_failure(self, excinfo):
        """Called when self.runtest() raises an exception."""
        if isinstance(excinfo.value, YamlException):
            return "\n".join(
                [
                    "usecase execution failed",
                    "   spec failed: {1!r}: {2!r}".format(*excinfo.value.args),
                    "   no further details known at this point.",
                ]
            )

    def reportinfo(self):
        return self.path, 0, f"usecase: {self.name}"


class YamlException(Exception):
    """Custom exception for error reporting."""

創建一個簡單的yaml文件

# test_simple.yaml
ok:
    sub1: sub1

hello:
    world: world
    some: other

如果你已經安裝了 PyYAML 或 YAML-parser解析器,那麼就可以執行yaml用例

nonpython $ pytest test_simple.yaml
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project/nonpython
collected 2 items

test_simple.yaml F.                                                  [100%]

================================= FAILURES =================================
______________________________ usecase: hello ______________________________
usecase execution failed
   spec failed: 'some': 'other'
   no further details known at this point.
========================= short test summary info ==========================
FAILED test_simple.yaml::hello
======================= 1 failed, 1 passed in 0.12s ========================

網上關於 pytest 插件開發的資料非常少,大部分都是停留在使用 pytest 寫用例的階段。
也有一些 pytest+yaml 的封裝,最終還是會寫的 py 文件去讀取 yaml 文件執行用例,並沒有達到真正意義上的把 yaml 文件當一個用例去執行。

pytest_collect_file 鉤子

先看下pytest_collect_file 鉤子的定義

def pytest_collect_file(
    file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
    """Create a :class:`~pytest.Collector` for the given path, or None if not relevant.

    The new node needs to have the specified ``parent`` as a parent.

    :param file_path: The path to analyze.
    :param path: The path to collect (deprecated).

    .. versionchanged:: 7.0.0
        The ``file_path`` parameter was added as a :class:`pathlib.Path`
        equivalent of the ``path`` parameter. The ``path`` parameter
        has been deprecated.
    """

這裡用到了3個參數

  • file_path 它是一個 pathlib.Path 對象, 收集到的文件路徑
  • path LEGACY_PATH(合法路徑), 收集到的文件路徑
  • parent Collector 收集器,用例文件.py 或者 .yml 文件的父目錄,也就是 python 的包 Package

v 7.0.0 版本的變更:
在 v 7.0.0 版本後,新增了一個 file_path 參數,它與原來的 path 功能是一樣的,原來的 path 參數會被棄用。

我們看下這2個參數變更前和變更後到底用什麼區別呢?

def pytest_collect_file(file_path: Path, path, parent):
    # 獲取文件.yml 文件,匹配規則
    if file_path.suffix == ".yml" and file_path.name.startswith("test"):
        print(file_path, type(file_path))
        print(path, type(path))
        print(parent, type(parent))
        return YamlFile.from_parent(parent, path=file_path)

運行 pytest -s
看到列印日誌

collecting ... D:\demo\demo_x2\case\test_login.yml <class 'pathlib.WindowsPath'>
D:\demo\demo_x2\case\test_login.yml <class '_pytest._py.path.LocalPath'>
<Package case> <class '_pytest.python.Package'>

原來的path參數(path.LocalPath),是通過os模組的path 獲取的文件路徑
最新的file_path 參數(pathlib.WindowsPath), 是通過pathlib 模組獲取的文件路徑。
pathlib 是 os模組的升級版,所以這裡做了一個細節的優化。

通過pytest_collect_file收集鉤子就可以找到.yml後綴,並且以test開頭的文件,會被當做用例返回。

pytest_ignore_collect 忽略收集

pytest_collect_file 勾選相反的一個忽略收集鉤子pytest_ignore_collect


[docs]@hookspec(firstresult=True)
def pytest_ignore_collect(
    collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
    """Return True to prevent considering this path for collection.

    This hook is consulted for all files and directories prior to calling
    more specific hooks.

    Stops at first non-None result, see :ref:`firstresult`.

    :param collection_path: The path to analyze.
    :param path: The path to analyze (deprecated).
    :param config: The pytest config object.

    .. versionchanged:: 7.0.0
        The ``collection_path`` parameter was added as a :class:`pathlib.Path`
        equivalent of the ``path`` parameter. The ``path`` parameter
        has been deprecated.
    """

也是傳3個參數

  • collection_path 收集到的用例文件路徑,pathlib.Path類
  • path 跟 collection_path作用一樣,被棄用了
  • config Config的實例

通過返回布爾值判斷是否收集該文件
舉個例子,當判斷用例文件名稱是test_login.yml 就不收集

def pytest_ignore_collect(collection_path: Path, path, config):
    # 返回布爾值(會根據返回值為 True 還是 False 來決定是否收集改路徑下的用例)
    if collection_path.name == 'test_x.yml':
        return True

運行後不會收集’test_x.yml’文件

Tags: