pytest 插件-編寫插件

2022-03-21 16:16 更新

為您自己的項目實現(xiàn)本地 ?conftest插件或可在許多項目(包括第三方項目)中使用的 pip 可安裝插件很容易。

一個插件包含一個或多個鉤子函數(shù)。pytest 通過調(diào)用以下插件的指定鉤子來實現(xiàn)配置、收集、運行和報告的各個方面:

  • 內(nèi)置插件:從 pytest 的內(nèi)部 ?_pytest? 目錄加載。
  • 外部插件:通過 ?setuptools入口點發(fā)現(xiàn)的模塊
  • conftest.py 插件:在測試目錄中自動發(fā)現(xiàn)的模塊

原則上,每個鉤子調(diào)用都是一個 ?1:N? Python 函數(shù)調(diào)用,其中 ?N是給定規(guī)范的已注冊實現(xiàn)函數(shù)的數(shù)量。 所有規(guī)范和實現(xiàn)都遵循 ?pytest_? 前綴命名約定,便于區(qū)分和查找。

工具啟動時插件的發(fā)現(xiàn)順序

pytest 在工具啟動時通過以下方式加載插件模塊:

  1. 通過掃描命令行中的 ?-p no:name? 選項并阻止加載該插件(即使是內(nèi)置插件也可以通過這種方式阻止)。 這發(fā)生在正常的命令行解析之前。
  2. 通過加載所有內(nèi)置插件。
  3. 通過掃描命令行以查找 ?-p name? 選項并加載指定的插件。 這發(fā)生在正常的命令行解析之前。
  4. 通過加載通過 ?setuptools入口點注冊的所有插件。
  5. 通過加載通過 ?PYTEST_PLUGINS環(huán)境變量指定的所有插件。
  6. 通過加載命令行調(diào)用推斷的所有 ?conftest.py? 文件:
    • 如果沒有指定測試路徑,則使用當(dāng)前目錄作為測試路徑

    • 如果存在,則加載 ?conftest.py? 和? test*/conftest.py ?相對于第一個測試路徑的目錄部分。加載 ?conftest.py? 文件后,加載其 ?pytest_plugins? 變量中指定的所有插件(如果存在)。
    • 請注意,pytest 在工具啟動時不會在更深的嵌套子目錄中找到 ?conftest.py? 文件。 將 ?conftest.py? 文件保存在頂層測試或項目根目錄中通常是個好主意。
  7. 通過遞歸加載 ?conftest.py? 文件中 ?pytest_plugins? 變量指定的所有插件。

conftest.py:本地每個目錄插件

本地 ?conftest.py? 插件包含特定于目錄的鉤子實現(xiàn)。 鉤子會話和測試運行活動將調(diào)用 ?conftest.py? 文件中定義的所有鉤子,這些鉤子更靠近文件系統(tǒng)的根目錄。 實現(xiàn) ?pytest_runtest_setup ?鉤子的示例,以便在a子目錄中調(diào)用測試但不為其他目錄調(diào)用:

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?”可能會產(chǎn)生歧義,因為在你的?PYTHONPATH?或?sys.path?中也可能有其他?conftest.py?文件。 因此,項目將 ?conftest.py? 放在包范圍內(nèi)或從不從 ?conftest.py? 文件中導(dǎo)入任何內(nèi)容是一種很好的做法。

由于pytest在啟動過程中發(fā)現(xiàn)插件的方式,一些鉤子應(yīng)該只在?plugins或位于?tests?根目錄下的?conftest.py?文件中實現(xiàn)。

編寫自己的插件

如果你想寫一個插件,你可以復(fù)制很多現(xiàn)實生活中的例子:

  • 一個自定義集合示例插件
  • 提供 pytest 自己的功能的內(nèi)置插件
  • 許多提供附加功能的外部插件

所有這些插件都實現(xiàn)了鉤子and/or ?fixture?來擴展和添加功能。

確保查看優(yōu)秀的 ?cookiecutter-pytest-plugin? 項目,這是一個用于創(chuàng)作插件的 ?cookiecutter模板。

該模板提供了一個很好的起點,其中包含一個工作插件、使用 ?tox運行的測試、一個全面的 ?README文件以及一個預(yù)配置的入口點。

也考慮將你的插件貢獻(xiàn)給 ?pytest-dev? 一旦它有一些滿意的用戶而不是你自己。

讓其他人可以安裝您的插件

如果你想讓你的插件在外部可用,你可以為你的發(fā)行版定義一個所謂的入口點,以便 pytest 找到你的插件模塊。 pytest 查找 ?pytest11入口點以發(fā)現(xiàn)其插件,因此您可以通過在 ?setuptools-invocation? 中定義它來使您的插件可用:

# sample ./setup.py file
from setuptools import setup

setup(
    name="myproject",
    packages=["myproject"],
    # the following makes a plugin available to pytest
    entry_points={"pytest11": ["name_of_plugin = myproject.pluginmodule"]},
    # custom PyPI classifier for pytest plugins
    classifiers=["Framework :: Pytest"],
)

如果以這種方式安裝包,pytest 將加載 ?myproject.pluginmodule? 作為可以定義鉤子的插件。

確保在您的 PyPI 分類器列表中包含 ?Framework :: Pytest?,以便用戶輕松找到您的插件。

斷言重寫

pytest 的主要功能之一是使用簡單的斷言語句和斷言失敗時表達(dá)式的詳細(xì)自省。 這是由斷言重寫提供的,它在解析的 AST 被編譯為字節(jié)碼之前對其進(jìn)行修改。 這是通過 PEP 302 導(dǎo)入鉤子完成的,該鉤子在 pytest 啟動時盡早安裝,并在導(dǎo)入模塊時執(zhí)行此重寫。 但是,由于我們不想測試與您將在生產(chǎn)中運行的字節(jié)碼不同的字節(jié)碼,因此此鉤子僅重寫測試模塊本身(由 ?python_files配置選項定義)以及作為插件一部分的任何模塊。 任何其他導(dǎo)入的模塊都不會被重寫,并且會發(fā)生正常的斷言行為。

如果您在需要啟用斷言重寫的其他模塊中有斷言助手,則需要在導(dǎo)入之前明確要求 pytest 重寫此模塊。

register_assert_rewrite(*names)

注冊一個或多個要在導(dǎo)入時重寫的模塊名稱。

此函數(shù)將確保此模塊或包內(nèi)的所有模塊將重寫其斷言語句。 因此,您應(yīng)該確保在實際導(dǎo)入模塊之前調(diào)用它,如果您是使用包的插件,通常在您的 ?__init__.py? 中。

  • ?raises?:?TypeError ?– 如果給定的模塊名稱不是字符串。
  • 參數(shù):?names (str)
  • 返回類型:?None?

當(dāng)您編寫使用包創(chuàng)建的 pytest 插件時,這一點尤其重要。 導(dǎo)入鉤子僅將 ?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?會被重寫。如果helper模塊還包含需要重寫的?assert?語句,則在導(dǎo)入之前,需要將其標(biāo)記為?assert?語句。最簡單的方法是在?__init__.py?模塊中標(biāo)記它以便重寫,當(dāng)包中的模塊被導(dǎo)入時,?__init__.py?模塊總是首先被導(dǎo)入的。這樣?plugin.py?仍然可以正常導(dǎo)入?helper.py?。?pytest_foo/__init__.py?的內(nèi)容將需要看起來像這樣:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

在測試模塊或 conftest 文件中Requiring/Loading插件

你可以使用?pytest_plugins?在測試模塊或?conftest.py?文件中?require?插件:

pytest_plugins = ["name1", "name2"]

當(dāng)?test?模塊或?conftest?插件被加載時,指定的插件也會被加載。任何模塊都可以作為插件,包括應(yīng)用程序的內(nèi)部模塊:

pytest_plugins = "myapp.testsupport.myplugin"

?pytest_plugins是遞歸處理的,所以注意上面的例子中如果?myapp.testsupport.myplugin? 也聲明了?pytest_plugins?,那么變量的內(nèi)容也會被加載為插件,以此類推。

不推薦使用在非根 ?conftest.py? 文件中使用 ?pytest_plugins? 變量的插件。

這很重要,因為 ?conftest.py? 文件實現(xiàn)了每個目錄的鉤子實現(xiàn),但是一旦插件被導(dǎo)入,它將影響整個目錄樹。 為了避免混淆,不推薦在任何不在測試根目錄中的 ?conftest.py? 文件中定義 ?pytest_plugins?,并且會引發(fā)警告。

這種機制使得在應(yīng)用程序甚至外部應(yīng)用程序中共享?fixture?變得很容易,而不需要使用?setuptools?的入口點技術(shù)創(chuàng)建外部插件。

?pytest_plugins導(dǎo)入的插件也將自動標(biāo)記為斷言重寫。 但是,要使該模塊生效,必須先不導(dǎo)入該模塊; 如果在處理 ?pytest_plugins語句時它已經(jīng)被導(dǎo)入,則會產(chǎn)生警告,并且插件內(nèi)的斷言將不會被重寫。 要解決此問題,您可以在導(dǎo)入模塊之前自己調(diào)用 ?pytest.register_assert_rewrite()? ,或者您可以安排代碼延遲導(dǎo)入,直到插件注冊后。

通過名稱訪問另一個插件

如果一個插件想要與另一個插件的代碼協(xié)作,它可以通過插件管理器獲取引用,如下所示:

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

如果要查看現(xiàn)有插件的名稱,請使用 ?--trace-config? 選項。

注冊自定義標(biāo)記

如果您的插件使用任何標(biāo)記,您應(yīng)該注冊它們,以便它們出現(xiàn)在 pytest 的幫助文本中并且不會引起虛假警告。 例如,以下插件將為所有用戶注冊 ?cool_marker和 ?mark_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的插件,可幫助您為插件代碼編寫測試。 該插件默認(rèn)禁用,因此您必須先啟用它才能使用它。

您可以通過將以下行添加到測試目錄中的 ?conftest.py? 文件中來做到這一點:

# content of conftest.py

pytest_plugins = ["pytester"]

或者,您可以使用?-p pyteste?r命令行選項調(diào)用pytest。

這將允許您使用?pytester fixture?來測試您的插件代碼。

讓我們用一個例子來演示你可以用這個插件做什么。假設(shè)我們開發(fā)了一個插件,它提供一個?fixture hello?,該?fixture?生成一個函數(shù),我們可以用一個可選參數(shù)調(diào)用這個函數(shù)。它將返回一個字符串值?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 "Hello {name}!".format(name=name)

    return _hello

現(xiàn)在,?pytester fixture為創(chuàng)建臨時?conftest.py?文件和測試文件提供了一個方便的API。它還允許我們運行測試并返回一個結(jié)果對象,通過這個對象我們可以斷言測試的結(jié)果。

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 之前,可以將示例復(fù)制到 ?pytester的隔離環(huán)境中。 這樣我們可以將測試的邏輯抽象到單獨的文件中,這對于更長的測試和/或更長的 ?conftest.py? 文件特別有用。

請注意,要使 ?pytester.copy_example? 正常工作,我們需要在 ?pytest.ini? 中設(shè)置 ?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-7.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 =============================


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號