撰寫外掛程式

為您自己的專案實作 本地 conftest 外掛程式 或在許多專案(包括第三方專案)中使用的 可透過 pip 安裝的外掛程式 非常容易。如果您只想使用外掛程式而不是撰寫外掛程式,請參閱 如何安裝和使用外掛程式

外掛程式包含一個或多個 hook 函數。撰寫 hooks 解釋了您如何自行撰寫 hook 函數的基本知識和細節。pytest 透過呼叫以下外掛程式的 良好指定的 hooks 來實作組態、收集、執行和報告的所有方面

原則上,每個 hook 呼叫都是一個 1:N Python 函數呼叫,其中 N 是給定規格的已註冊實作函數的數量。所有規格和實作都遵循 pytest_ 字首命名慣例,使其易於區分和查找。

工具啟動時的外掛程式發現順序

pytest 在工具啟動時以下列方式載入外掛程式模組

  1. 透過掃描命令列以尋找 -p no:name 選項,並阻止載入該外掛程式(即使是內建外掛程式也可以透過這種方式阻止)。這發生在正常命令列解析之前。

  2. 透過載入所有內建外掛程式。

  3. 透過掃描命令列以尋找 -p name 選項,並載入指定的外掛程式。這發生在正常命令列解析之前。

  4. 透過載入透過已安裝第三方套件 entry points 註冊的所有外掛程式,除非設定了 PYTEST_DISABLE_PLUGIN_AUTOLOAD 環境變數。

  5. 透過載入透過 PYTEST_PLUGINS 環境變數指定的所有外掛程式。

  6. 透過載入所有「初始」 conftest.py 檔案

    • 確定測試路徑:在命令列上指定,否則在 testpaths 中定義(如果已定義並從 rootdir 執行),否則為目前目錄

    • 對於每個測試路徑,如果存在,則載入相對於測試路徑目錄部分的 conftest.pytest*/conftest.py。在載入 conftest.py 檔案之前,載入其所有父目錄中的 conftest.py 檔案。在載入 conftest.py 檔案之後,遞迴載入其 pytest_plugins 變數中指定的所有外掛程式(如果存在)。

conftest.py:本地每個目錄的外掛程式

本地 conftest.py 外掛程式包含目錄特定的 hook 實作。Hook Session 和測試執行活動將調用在更靠近檔案系統根目錄的 conftest.py 檔案中定義的所有 hooks。實作 pytest_runtest_setup hook 的範例,以便為 a 子目錄中的測試呼叫,但不為其他目錄呼叫

a/conftest.py:
    def pytest_runtest_setup(item):
        # called for running each test in 'a' directory
        print("setting up", item)

a/test_sub.py:
    def test_sub():
        pass

test_flat.py:
    def test_flat():
        pass

以下是如何執行它的方法

pytest test_flat.py --capture=no  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "setting up"

注意

如果您有不在 python 套件目錄(即包含 __init__.py 的目錄)中的 conftest.py 檔案,則「import conftest」可能會模稜兩可,因為您的 PYTHONPATHsys.path 上也可能存在其他 conftest.py 檔案。因此,專案的最佳實務是將 conftest.py 放在套件範圍下,或永遠不要從 conftest.py 檔案匯入任何內容。

另請參閱:pytest 導入機制和 sys.path/PYTHONPATH

注意

由於 pytest 在啟動期間發現外掛程式的方式,某些 hooks 無法在不是 初始 的 conftest.py 檔案中實作。請參閱每個 hook 的文件以瞭解詳細資訊。

撰寫您自己的外掛程式

如果您想撰寫外掛程式,您可以從許多真實範例中複製

所有這些外掛程式都實作 hooks 和/或 fixtures 以擴展和新增功能。

注意

請務必查看出色的 cookiecutter-pytest-plugin 專案,它是一個用於撰寫外掛程式的 cookiecutter 範本

該範本提供了一個絕佳的起點,其中包含一個可運作的外掛程式、使用 tox 執行的測試、全面的 README 檔案以及預先設定的 entry-point。

也請考慮在您的外掛程式擁有除您自己以外的一些快樂使用者後,將您的外掛程式貢獻給 pytest-dev

讓其他人可以安裝您的外掛程式

如果您想讓您的外掛程式在外部可用,您可以為您的發行版定義一個所謂的 entry point,以便 pytest 找到您的外掛程式模組。Entry points 是 封裝工具 提供的一項功能。

pytest 查找 pytest11 entrypoint 以發現其外掛程式,因此您可以透過在您的 pyproject.toml 檔案中定義它來使您的外掛程式可用。

# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myproject"
classifiers = [
    "Framework :: Pytest",
]

[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"

如果以這種方式安裝套件,pytest 將載入 myproject.pluginmodule 作為可以定義 hooks 的外掛程式。使用 pytest --trace-config 確認註冊

注意

請務必在您的 PyPI 分類器 列表中包含 Framework :: Pytest,以便使用者可以輕鬆找到您的外掛程式。

斷言重寫

pytest 的主要功能之一是使用簡單的 assert 語句以及在斷言失敗時對表達式進行詳細的內省。這是由「斷言重寫」提供的,它會在解析後的 AST 在編譯為位元組碼之前修改它。這是透過 PEP 302 匯入 hook 完成的,該 hook 在 pytest 啟動時早期安裝,並在匯入模組時執行此重寫。但是,由於我們不希望測試與您在生產環境中執行的位元組碼不同的位元組碼,因此此 hook 僅重寫測試模組本身(由 python_files 組態選項定義),以及任何屬於外掛程式一部分的模組。任何其他匯入的模組都不會被重寫,並且會發生正常的斷言行為。

如果您在其他模組中有斷言輔助程式,您需要在其中啟用斷言重寫,則您需要明確要求 pytest 在匯入此模組之前重寫它。

register_assert_rewrite(*names)[source]

註冊一個或多個模組名稱以在匯入時重寫。

此函數將確保此模組或套件內的所有模組都將重寫其 assert 語句。因此,您應確保在實際匯入模組之前呼叫此函數,通常是在您使用套件的外掛程式的 __init__.py 中。

參數:

names (str) – 要註冊的模組名稱。

當您撰寫使用套件建立的 pytest 外掛程式時,這尤其重要。匯入 hook 僅將 conftest.py 檔案和 pytest11 entrypoint 中列出的任何模組視為外掛程式。作為範例,請考慮以下套件

pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py

以及以下典型的 setup.py 摘錄

setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)

在這種情況下,只會重寫 pytest_foo/plugin.py。如果輔助程式模組也包含需要重寫的 assert 語句,則需要在匯入之前將其標記為這樣。最簡單的方法是在 __init__.py 模組內將其標記為重寫,當匯入套件內的模組時,該模組將始終首先匯入。這樣,plugin.py 仍然可以正常匯入 helper.pypytest_foo/__init__.py 的內容然後需要看起來像這樣

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

在測試模組或 conftest 檔案中要求/載入外掛程式

您可以使用 pytest_plugins 在測試模組或 conftest.py 檔案中要求外掛程式

pytest_plugins = ["name1", "name2"]

當載入測試模組或 conftest 外掛程式時,也會載入指定的外掛程式。任何模組都可以被視為外掛程式,包括內部應用程式模組

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins 是遞迴處理的,因此請注意,在上面的範例中,如果 myapp.testsupport.myplugin 也宣告了 pytest_plugins,則變數的內容也將作為外掛程式載入,依此類推。

注意

在非根 conftest.py 檔案中使用 pytest_plugins 變數要求外掛程式已被棄用。

這很重要,因為 conftest.py 檔案實作每個目錄的 hook 實作,但一旦匯入外掛程式,它將影響整個目錄樹。為了避免混淆,在任何不在測試根目錄中的 conftest.py 檔案中定義 pytest_plugins 已被棄用,並且會引發警告。

這種機制使得在應用程式甚至外部應用程式中共享 fixtures 變得容易,而無需使用 entry point 封裝元資料 技術來建立外部外掛程式。

pytest_plugins 匯入的外掛程式也會自動標記為斷言重寫(請參閱 pytest.register_assert_rewrite())。但是,為了使其生效,模組不得已匯入;如果在處理 pytest_plugins 語句時已匯入,則會導致警告,並且外掛程式內的斷言將不會被重寫。要修正此問題,您可以在匯入模組之前自行呼叫 pytest.register_assert_rewrite(),或者您可以安排程式碼以延遲匯入,直到外掛程式註冊後。

按名稱存取另一個外掛程式

如果外掛程式想要與另一個外掛程式的程式碼協作,它可以透過外掛程式管理器取得參考,如下所示

plugin = config.pluginmanager.get_plugin("name_of_plugin")

如果您想查看現有外掛程式的名稱,請使用 --trace-config 選項。

註冊自訂標記

如果您的外掛程式使用任何標記,您應該註冊它們,以便它們出現在 pytest 的說明文字中,並且不會導致虛假的警告。例如,以下外掛程式將為所有使用者註冊 cool_markermark_with

def pytest_configure(config):
    config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
    config.addinivalue_line(
        "markers", "mark_with(arg, arg2): this marker takes arguments."
    )

測試外掛程式

pytest 隨附一個名為 pytester 的外掛程式,可協助您為外掛程式程式碼撰寫測試。預設情況下,該外掛程式已停用,因此您必須先啟用它才能使用它。

您可以透過將以下行新增到測試目錄中的 conftest.py 檔案來執行此操作

# content of conftest.py

pytest_plugins = ["pytester"]

或者,您可以使用 -p pytester 命令列選項調用 pytest。

這將允許您使用 pytester fixture 來測試您的外掛程式程式碼。

讓我們用一個範例示範您可以使用外掛程式做什麼。假設我們開發了一個外掛程式,該外掛程式提供了一個 fixture hello,它會產生一個函數,我們可以調用這個函數,並帶有一個可選參數。如果我們不提供值,它將傳回 Hello World! 的字串值;如果我們提供字串值,則傳回 Hello {value}!

import pytest


def pytest_addoption(parser):
    group = parser.getgroup("helloworld")
    group.addoption(
        "--name",
        action="store",
        dest="name",
        default="World",
        help='Default "name" for hello().',
    )


@pytest.fixture
def hello(request):
    name = request.config.getoption("name")

    def _hello(name=None):
        if not name:
            name = request.config.getoption("name")
        return f"Hello {name}!"

    return _hello

現在,pytester fixture 提供了一個方便的 API,用於建立臨時 conftest.py 檔案和測試檔案。它還允許我們執行測試並傳回結果物件,我們可以透過該物件斷言測試的結果。

def test_hello(pytester):
    """Make sure that our plugin works."""

    # create a temporary conftest.py file
    pytester.makeconftest(
        """
        import pytest

        @pytest.fixture(params=[
            "Brianna",
            "Andreas",
            "Floris",
        ])
        def name(request):
            return request.param
    """
    )

    # create a temporary pytest test file
    pytester.makepyfile(
        """
        def test_hello_default(hello):
            assert hello() == "Hello World!"

        def test_hello_name(hello, name):
            assert hello(name) == "Hello {0}!".format(name)
    """
    )

    # run all tests with pytest
    result = pytester.runpytest()

    # check that all 4 tests passed
    result.assert_outcomes(passed=4)

此外,可以在對其執行 pytest 之前將範例複製到 pytester 的隔離環境中。這樣,我們可以將經過測試的邏輯抽象化為單獨的檔案,這對於較長的測試和/或較長的 conftest.py 檔案特別有用。

請注意,為了使 pytester.copy_example 能夠運作,我們需要在 pytest.ini 中設定 pytester_example_dir,以告知 pytest 在哪裡尋找範例檔案。

# content of pytest.ini
[pytest]
pytester_example_dir = .
# content of test_example.py


def test_plugin(pytester):
    pytester.copy_example("test_example.py")
    pytester.runpytest("-k", "test_example")


def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

============================ 2 passed in 0.12s =============================

有關 runpytest() 傳回的結果物件以及它提供的方法的更多資訊,請查看 RunResult 文件。