pytest 插件-編寫插件

2022-03-21 16:16 更新

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

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

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

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

工具啟動(dòng)時(shí)插件的發(fā)現(xiàn)順序

pytest 在工具啟動(dòng)時(shí)通過以下方式加載插件模塊:

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

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

conftest.py:本地每個(gè)目錄插件

本地 ?conftest.py? 插件包含特定于目錄的鉤子實(shí)現(xiàn)。 鉤子會(huì)話和測(cè)試運(yùn)行活動(dòng)將調(diào)用 ?conftest.py? 文件中定義的所有鉤子,這些鉤子更靠近文件系統(tǒng)的根目錄。 實(shí)現(xiàn) ?pytest_runtest_setup ?鉤子的示例,以便在a子目錄中調(diào)用測(cè)試但不為其他目錄調(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

以下是您可以如何運(yùn)行它:

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

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

編寫自己的插件

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

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

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

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

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

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

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

如果你想讓你的插件在外部可用,你可以為你的發(fā)行版定義一個(gè)所謂的入口點(diǎn),以便 pytest 找到你的插件模塊。 pytest 查找 ?pytest11入口點(diǎn)以發(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 的主要功能之一是使用簡(jiǎn)單的斷言語句和斷言失敗時(shí)表達(dá)式的詳細(xì)自省。 這是由斷言重寫提供的,它在解析的 AST 被編譯為字節(jié)碼之前對(duì)其進(jìn)行修改。 這是通過 PEP 302 導(dǎo)入鉤子完成的,該鉤子在 pytest 啟動(dòng)時(shí)盡早安裝,并在導(dǎo)入模塊時(shí)執(zhí)行此重寫。 但是,由于我們不想測(cè)試與您將在生產(chǎn)中運(yùn)行的字節(jié)碼不同的字節(jié)碼,因此此鉤子僅重寫測(cè)試模塊本身(由 ?python_files配置選項(xiàng)定義)以及作為插件一部分的任何模塊。 任何其他導(dǎo)入的模塊都不會(huì)被重寫,并且會(huì)發(fā)生正常的斷言行為。

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

register_assert_rewrite(*names)

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

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

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

當(dāng)您編寫使用包創(chuàng)建的 pytest 插件時(shí),這一點(diǎn)尤其重要。 導(dǎo)入鉤子僅將 ?conftest.py? 文件和 ?pytest11? 入口點(diǎn)中列出的任何模塊視為插件。 例如,考慮以下包:

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?會(huì)被重寫。如果helper模塊還包含需要重寫的?assert?語句,則在導(dǎo)入之前,需要將其標(biāo)記為?assert?語句。最簡(jiǎn)單的方法是在?__init__.py?模塊中標(biāo)記它以便重寫,當(dāng)包中的模塊被導(dǎo)入時(shí),?__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")

在測(cè)試模塊或 conftest 文件中Requiring/Loading插件

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

pytest_plugins = ["name1", "name2"]

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

pytest_plugins = "myapp.testsupport.myplugin"

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

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

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

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

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

通過名稱訪問另一個(gè)插件

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

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

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

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

如果您的插件使用任何標(biāo)記,您應(yīng)該注冊(cè)它們,以便它們出現(xiàn)在 pytest 的幫助文本中并且不會(huì)引起虛假警告。 例如,以下插件將為所有用戶注冊(cè) ?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."
    )

測(cè)試插件

pytest 附帶一個(gè)名為 ?pytester的插件,可幫助您為插件代碼編寫測(cè)試。 該插件默認(rèn)禁用,因此您必須先啟用它才能使用它。

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

# content of conftest.py

pytest_plugins = ["pytester"]

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

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

讓我們用一個(gè)例子來演示你可以用這個(gè)插件做什么。假設(shè)我們開發(fā)了一個(gè)插件,它提供一個(gè)?fixture hello?,該?fixture?生成一個(gè)函數(shù),我們可以用一個(gè)可選參數(shù)調(diào)用這個(gè)函數(shù)。它將返回一個(gè)字符串值?Hello World!?如果我們不提供一個(gè)值或?Hello {value}!?如果我們提供一個(gè)字符串值。

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)建臨時(shí)?conftest.py?文件和測(cè)試文件提供了一個(gè)方便的API。它還允許我們運(yùn)行測(cè)試并返回一個(gè)結(jié)果對(duì)象,通過這個(gè)對(duì)象我們可以斷言測(cè)試的結(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)

此外,在運(yùn)行 pytest 之前,可以將示例復(fù)制到 ?pytester的隔離環(huán)境中。 這樣我們可以將測(cè)試的邏輯抽象到單獨(dú)的文件中,這對(duì)于更長的測(cè)試和/或更長的 ?conftest.py? 文件特別有用。

請(qǐng)注意,要使 ?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)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)