撰寫外掛程式

可以輕鬆實作專案的 本地 conftest 外掛程式可透過 pip 安裝的外掛程式,後者可用於許多專案,包括第三方專案。如果你只想使用外掛程式,但不想撰寫外掛程式,請參閱 如何安裝和使用外掛程式

外掛程式包含一個或多個掛勾函式。 撰寫掛勾 說明如何撰寫掛勾函式的基礎知識和詳細資訊。 pytest 透過呼叫下列外掛程式的 明確指定的掛勾 來實作設定、收集、執行和報告的各個面向

原則上,每個掛勾呼叫都是 1:N Python 函式呼叫,其中 N 是針對特定規格註冊的實作函式數目。所有規格和實作都遵循 pytest_ 前置命名慣例,讓它們易於辨識和尋找。

工具啟動時的外掛程式偵測順序

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

  1. 掃描命令列的 -p no:name 選項,並封鎖該外掛程式載入(甚至內建外掛程式也可以用這種方式封鎖)。這會在一般命令列解析之前發生。

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

  3. 掃描命令列的 -p name 選項,並載入指定的外掛程式。這會在一般命令列解析之前發生。

  4. 載入所有透過 setuptools 進入點 註冊的外掛程式。

  5. 透過載入所有透過 PYTEST_PLUGINS 環境變數指定的插件來載入。

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

    • 決定測試路徑:在命令列上指定,否則在 testpaths 中定義且從 rootdir 執行,否則為目前目錄

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

conftest.py:每個目錄的區域插件

區域 conftest.py 插件包含特定於目錄的掛勾實作。掛勾會話和測試執行活動將呼叫在更接近檔案系統根目錄的 conftest.py 檔案中定義的所有掛勾。實作 pytest_runtest_setup 掛勾的範例,以便在 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"

注意

如果您有 conftest.py 檔案,但它不在 Python 套件目錄中(即包含 __init__.py 的檔案),則「import conftest」可能會模稜兩可,因為您的 PYTHONPATHsys.path 中也可能有其他 conftest.py 檔案。因此,專案的良好做法是將 conftest.py 放在套件範圍內,或從不從 conftest.py 檔案匯入任何內容。

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

注意

有些掛勾無法在不是 初始 的 conftest.py 檔案中實作,這是因為 pytest 在啟動期間偵測插件的方式。有關詳細資訊,請參閱每個掛勾的文件。

撰寫自己的外掛程式

如果您想撰寫外掛程式,有許多現實生活中的範例可以複製

所有這些外掛程式都實作 掛勾 和/或 固定裝置 以擴充和新增功能。

注意

請務必查看優秀的 cookiecutter-pytest-plugin 專案,它是 cookiecutter 範本,用於撰寫外掛程式。

此範本提供一個優秀的起點,其中包含一個運作中的外掛程式、使用 tox 執行的測試、一個全面的 README 檔案以及一個預先設定的進入點。

此外,請考慮 將您的外掛程式貢獻給 pytest-dev,一旦它除了您之外還有一些滿意的使用者。

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

如果您想讓您的外掛程式在外部可用,您可以為您的發行版定義一個所謂的進入點,以便 pytest 找到您的外掛程式模組。進入點是由 setuptools 提供的功能。

pytest 查詢 pytest11 進入點以找出其外掛程式,因此您可以透過在 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 作為可定義 掛勾 的外掛程式。使用 pytest --trace-config 確認註冊。

注意

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

宣告重寫

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

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

register_assert_rewrite(*names)[source]

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

此函式會確保此模組或套件內的所有模組都會重寫其宣告陳述。因此,您應該確保在實際匯入模組之前呼叫此函式,通常是在 __init__.py 中,如果您是使用套件的外掛程式。

參數:

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

當您使用套件建立 pytest 外掛程式時,這特別重要。匯入掛鉤只會處理 conftest.py 檔案和任何在 pytest11 進入點中列為外掛程式的模組。以下套件為例

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.py。然後,pytest_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 檔案實作每個目錄的掛鉤實作,但一旦匯入外掛程式,它將影響整個目錄樹。為了避免混淆,在任何未位於測試根目錄的 conftest.py 檔案中定義 pytest_plugins 已不建議使用,並且會產生警告。

此機制讓在應用程式或甚至外部應用程式中分享固定裝置變得容易,而不需要使用 setuptools 的進入點技術來建立外部外掛程式。

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 固定裝置來測試你的外掛程式程式碼。

讓我們透過一個範例來說明你可以使用此外掛程式做什麼。假設我們開發了一個提供固定裝置 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 固定裝置提供了一個便利的 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 文件。