Pytest官方教程-19-插件編寫

目錄:

  1. 安裝及入門
  2. 使用和調用方法
  3. 原有TestSuite使用方法
  4. 斷言的編寫和報告
  5. Pytest fixtures:清晰 模塊化 易擴展
  6. 使用Marks標記測試用例
  7. Monkeypatching/對模塊和環境進行Mock
  8. 使用tmp目錄和文件
  9. 捕獲stdout及stderr輸出
  10. 捕獲警告信息
  11. 模塊及測試文件中集成doctest測試
  12. skip及xfail: 處理不能成功的測試用例
  13. Fixture方法及測試用例的參數化
  14. 緩存: 使用跨執行狀態
  15. unittest.TestCase支持
  16. 運行Nose用例
  17. 經典xUnit風格的setup/teardown
  18. 安裝和使用插件
  19. 插件編寫
  20. 編寫鉤子(hook)方法
  21. 運行日志
  22. API參考
    1. 方法(Functions)
    2. 標記(Marks)
    3. 鉤子(Hooks)
    4. 裝置(Fixtures)
    5. 對象(Objects)
    6. 特殊變量(Special Variables)
    7. 環境變量(Environment Variables)
    8. 配置選項(Configuration Options)
  23. 優質集成實踐
  24. 片狀測試
  25. Pytest導入機制及sys.path/PYTHONPATH
  26. 配置選項
  27. 示例及自定義技巧
  28. Bash自動補全設置

插件編寫

很容易為你自己的項目實現本地conftest插件或可以在許多項目中使用的可安裝的插件,包括第三方項目。如果你只想使用但不能編寫插件,請參閱安裝和使用插件。

插件包含一個或多個鉤子(hooks)方法函數。編寫鉤子(hooks)方法 解釋了如何自己編寫鉤子(hooks)方法函數的基礎知識和細節。pytest通過調用以下插件的指定掛鉤來實現配置,收集,運行和報告的所有方面:

原則上,每個鉤子(hooks)方法調用都是一個1:NPython函數調用,其中N是給定規范的已注冊實現函數的數量。所有規范和實現都遵循pytest_前綴命名約定,使其易于區分和查找。

工具啟動時的插件發現順序

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

  • 通過加載所有內置插件

  • 通過加載通過setuptools入口點注冊的所有插件。

  • 通過預掃描選項的命令行并在實際命令行解析之前加載指定的插件。-p name

  • 通過conftest.py命令行調用推斷加載所有文件:

    • 如果未指定測試路徑,則使用當前dir作為測試路徑
    • 如果存在,則加載conftest.pytest*/conftest.py相對于第一個測試路徑的目錄部分。

    請注意,pytest conftest.py在工具啟動時沒有在更深的嵌套子目錄中找到文件。將conftest.py文件保存在頂級測試或項目根目錄中通常是個好主意。

  • 通過遞歸加載文件中pytest_plugins變量指定的所有插件 conftest.py

conftest.py:本地每目錄插件

本地conftest.py插件包含特定于目錄的鉤子(hooks)方法實現。Hook Session和測試運行活動將調用conftest.py靠近文件系統根目錄的文件中定義的所有掛鉤。實現pytest_runtest_setup鉤子(hooks)方法的示例, 以便在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”可能不明確,因為conftest.pyPYTHONPATH或者也可能有其他 文件sys.path。因此,項目要么放在conftest.py 包范圍內,要么永遠不從conftest.py文件中導入任何內容, 這是一種很好的做法。

另請參見:pytest import mechanisms和sys.path / PYTHONPATH

編寫自己的插件

如果你想編寫插件,可以從中復制許多現實示例:

所有這些插件都實現了鉤子(hooks)方法和/或固定裝置 以擴展和添加功能。

注意

請務必查看優秀 的cookiecutter-pytest-plugin 項目,該項目是 用于創作插件的cookiecutter模板

該模板提供了一個很好的起點,包括一個工作插件,使用tox運行的測試,一個全面的README文件以及一個預先配置的入口點。

另外考慮將你的插件貢獻給pytest-dev 一旦它擁有一些非自己的快樂用戶。

使你的插件可以被他人安裝

如果你想讓你的插件在外部可用,你可以為你的發行版定義一個所謂的入口點,以便pytest找到你的插件模塊。入口點是setuptools提供的功能。pytest查找pytest11入口點以發現其插件,因此你可以通過在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"],
)

如果以這種方式安裝包,pytestmyproject.pluginmodule作為可以定義掛鉤的插件 加載 。

注意

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

斷言重寫

其中一個主要特性pytest是使用普通的斷言語句以及斷言失敗時表達式的詳細內省。這是由“斷言重寫”提供的,它在編譯為字節碼之前修改了解析的AST。這是通過一個完成的PEP 302導入掛鉤,在pytest啟動時及早安裝 ,并在導入模塊時執行此重寫。但是,由于我們不想測試不同的字節碼,因此你將在生產中運行此掛鉤僅重寫測試模塊本身以及作為插件一部分的任何模塊。任何其他導入的模塊都不會被重寫,并且會發生正常的斷言行為。

如果你在其他模塊中有斷言助手,你需要啟用斷言重寫,你需要pytest 在導入之前明確要求重寫這個模塊。

注冊一個或多個要在導入時重寫的模塊名稱。

此函數將確保此模塊或程序包內的所有模塊將重寫其assert語句。因此,你應確保在實際導入模塊之前調用此方法,如果你是使用包的插件,則通常在init.py中調用。

<colgroup><col class="field-name" style="hyphens: manual;"><col class="field-body"></colgroup>
| 舉: | TypeError - 如果給定的模塊名稱不是字符串。 |

當你編寫使用包創建的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被重寫。如果輔助模塊還包含需要重寫的斷言語句,則需要在導入之前將其標記為這樣。通過將其標記為在__init__.py模塊內部進行重寫,這是最簡單的,當導入包中的 模塊時,將始終首先導入該模塊。這種方式plugin.py仍然可以helper.py正常導入。然后,內容 pytest_foo/__init__.py將需要如下所示:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

在測試模塊或conftest文件中要求/加載插件

你可以在測試模塊或這樣的conftest.py文件中要求插件:

pytest_plugins = ["name1", "name2"]

加載測試模塊或conftest插件時,也會加載指定的插件。任何模塊都可以作為插件祝福,包括內部應用程序模塊:

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins變量是遞歸處理的,所以請注意,在上面的示例中,如果myapp.testsupport.myplugin也聲明pytest_plugins,變量的內容也將作為插件加載,依此類推。

注意

pytest_plugins不建議使用非根conftest.py文件中使用變量的 插件。

這很重要,因為conftest.py文件實現了每個目錄的鉤子(hooks)方法實現,但是一旦導入了插件,它就會影響整個目錄樹。為了避免混淆,不推薦pytest_plugins在任何conftest.py不在測試根目錄中的文件中進行定義 ,并將發出警告。

這種機制使得在應用程序甚至外部應用程序中共享裝置變得容易,而無需使用setuptools入口點技術創建外部插件。

導入的插件pytest_plugins也會自動標記為斷言重寫(請參閱參考資料pytest.register_assert_rewrite())。但是,為了使其具有任何效果,必須不必導入模塊; 如果在pytest_plugins處理語句時已經導入它 ,則會產生警告,并且不會重寫插件內的斷言。要解決此問題,你可以pytest.register_assert_rewrite()在導入模塊之前自行調用,也可以安排代碼以延遲導入,直到注冊插件為止。

按名稱訪問另一個插件

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

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

如果要查看現有插件的名稱,請使用該--trace-config選項。

測試插件

pytest附帶一個名為的插件pytester,可幫助你為插件代碼編寫測試。默認情況下,該插件處于禁用狀態,因此你必須先啟用它,然后才能使用它。

你可以通過conftest.py將以下行添加到測試目錄中的文件來執行此操作:

# content of conftest.py

pytest_plugins = ["pytester"]

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

這將允許你使用testdir fixture來測試你的插件代碼。

讓我們用一個例子演示你可以用插件做什么。想象一下,我們開發了一個插件,它提供了一個hello產生函數的fixture ,我們可以用一個可選參數調用這個函數。如果我們不提供值或者我們提供字符串值,它將返回字符串值。Hello World!``Hello {value}!

# -*- coding: utf-8 -*-

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

現在,testdirfixture提供了一個方便的API來創建臨時 conftest.py文件和測試文件。它還允許我們運行測試并返回一個結果對象,通過它我們可以斷言測試的結果。

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

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

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

    # create a temporary pytest test file
    testdir.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 = testdir.runpytest()

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

另外,可以在運行pytest之前復制示例文件夾的示例

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

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

def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

============================= warnings summary =============================
test_example.py::test_plugin
  $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
    testdir.copy_example("test_example.py")

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================== 2 passed, 1 warnings in 0.12 seconds ===================

有關runpytest()返回的結果對象及其提供的方法的更多信息,請查看RunResult文檔。

編寫鉤子(hooks)方法函數

鉤子(hooks)方法函數驗證和執行

pytest為任何給定的鉤子(hooks)方法規范調用已注冊插件的鉤子(hooks)方法函數。讓我們看一下鉤子(hooks)方法的典型鉤子(hooks)方法函數,pytest在收集完所有測試項目后調用。pytest_collection_modifyitems(session, config,items)

當我們pytest_collection_modifyitems在插件中實現一個函數時,pytest將在注冊期間驗證你是否使用了與規范匹配的參數名稱,如果沒有則拯救。

讓我們看一下可能的實現:

def pytest_collection_modifyitems(config, items):
    # called after collection is completed
    # you can modify the ``items`` list
    ...

這里,pytest將傳入config(pytest配置對象)和items(收集的測試項列表),但不會傳入session參數,因為我們沒有在函數簽名中列出它。這種動態的“修剪”參數允許pytest“未來兼容”:我們可以引入新的鉤子(hooks)方法命名參數而不破壞現有鉤子(hooks)方法實現的簽名。這是pytest插件的一般長期兼容性的原因之一。

請注意,除了pytest_runtest_*不允許引發異常之外的鉤子(hooks)方法函數。這樣做會打破pytest運行。

firstresult:首先停止非無結果

大多數對pytest鉤子(hooks)方法的調用都會產生一個結果列表,其中包含被調用鉤子(hooks)方法函數的所有非None結果。

一些鉤子(hooks)方法規范使用該firstresult=True選項,以便鉤子(hooks)方法調用僅執行,直到N個注冊函數中的第一個返回非None結果,然后將其作為整個鉤子(hooks)方法調用的結果。在這種情況下,不會調用其余的鉤子(hooks)方法函數。

hookwrapper:在其他鉤子(hooks)方法周圍執行

版本2.7中的新功能。

pytest插件可以實現鉤子(hooks)方法包裝器,它包裝其他鉤子(hooks)方法實現的執行。鉤子(hooks)方法包裝器是一個生成器函數,它只產生一次。當pytest調用鉤子(hooks)方法時,它首先執行鉤子(hooks)方法包裝器并傳遞與常規鉤子(hooks)方法相同的參數。

在鉤子(hooks)方法包裝器的屈服點,pytest將執行下一個鉤子(hooks)方法實現,并以Result封裝結果或異常信息的實例的形式將其結果返回到屈服點。因此,屈服點本身通常不會引發異常(除非存在錯誤)。

以下是鉤子(hooks)方法包裝器的示例定義:

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    outcome = yield
    # outcome.excinfo may be None or a (cls, val, tb) tuple

    res = outcome.get_result()  # will raise if outcome was exception

    post_process_result(res)

    outcome.force_result(new_res)  # to override the return value to the plugin system

請注意,鉤子(hooks)方法包裝器本身不返回結果,它們只是圍繞實際的鉤子(hooks)方法實現執行跟蹤或其他副作用。如果底層鉤子(hooks)方法的結果是一個可變對象,它們可能會修改該結果,但最好避免它。

有關更多信息,請參閱插件文檔

鉤子(hooks)方法函數排序/調用示例

對于任何給定的鉤子(hooks)方法規范,可能存在多個實現,因此我們通常將hook執行視為 1:N函數調用,其中N是已注冊函數的數量。有一些方法可以影響鉤子(hooks)方法實現是在其他人之前還是之后,即在N-sized函數列表中的位置:

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    # will execute as early as possible
    ...

# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    # will execute as late as possible
    ...

# Plugin 3
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    outcome = yield
    # will execute after all non-hookwrappers executed

這是執行的順序:

  1. Plugin3的pytest_collection_modifyitems被調用直到屈服點,因為它是一個鉤子(hooks)方法包裝器。
  2. 調用Plugin1的pytest_collection_modifyitems是因為它標有tryfirst=True
  3. 調用Plugin2的pytest_collection_modifyitems因為它被標記trylast=True(但即使沒有這個標記,它也會在Plugin1之后出現)。
  4. 插件3的pytest_collection_modifyitems然后在屈服點之后執行代碼。yield接收一個Result實例,該實例封裝了調用非包裝器的結果。包裝不得修改結果。

這是可能的使用tryfirst,并trylast結合還 hookwrapper=True處于這種情況下,它會影響彼此之間hookwrappers的排序。

聲明新鉤子(hooks)方法

插件和conftest.py文件可以聲明新鉤子(hooks)方法,然后可以由其他插件實現,以便改變行為或與新插件交互:

在插件注冊時調用,允許通過調用添加新的掛鉤 。pluginmanager.add_hookspecs(module_or_class, prefix)
參數: | pluginmanager_pytest.config.PytestPluginManager) - pytest插件管理器

注意:
這個鉤子(hooks)方法與之不相容hookwrapper=True

鉤子(hooks)方法通常被聲明為do-nothing函數,它們只包含描述何時調用鉤子(hooks)方法以及期望返回值的文檔。

有關示例,請參閱xdist中newhooks.py

可選擇使用第三方插件的鉤子(hooks)方法

由于標準的驗證機制,如上所述使用插件中的新鉤子(hooks)方法可能有點棘手:如果你依賴未安裝的插件,驗證將失敗并且錯誤消息對你的用戶沒有多大意義。

一種方法是將鉤子(hooks)方法實現推遲到新的插件,而不是直接在插件模塊中聲明鉤子(hooks)方法函數,例如:

# contents of myplugin.py

class DeferPlugin(object):
    """Simple plugin to defer pytest-xdist hook functions."""

    def pytest_testnodedown(self, node, error):
        """standard xdist hook function.
 """

def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

這具有額外的好處,允許你根據安裝的插件有條件地安裝掛鉤。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容