目錄:
- 安裝及入門
- 使用和調用方法
- 原有TestSuite使用方法
- 斷言的編寫和報告
- Pytest fixtures:清晰 模塊化 易擴展
- 使用Marks標記測試用例
- Monkeypatching/對模塊和環境進行Mock
- 使用tmp目錄和文件
- 捕獲stdout及stderr輸出
- 捕獲警告信息
- 模塊及測試文件中集成doctest測試
- skip及xfail: 處理不能成功的測試用例
- Fixture方法及測試用例的參數化
- 緩存: 使用跨執行狀態
- unittest.TestCase支持
- 運行Nose用例
- 經典xUnit風格的setup/teardown
- 安裝和使用插件
- 插件編寫
- 編寫鉤子(hook)方法
- 運行日志
- API參考
- 優質集成實踐
- 片狀測試
- Pytest導入機制及sys.path/PYTHONPATH
- 配置選項
- 示例及自定義技巧
- Bash自動補全設置
插件編寫
很容易為你自己的項目實現本地conftest插件或可以在許多項目中使用的可安裝的插件,包括第三方項目。如果你只想使用但不能編寫插件,請參閱安裝和使用插件。
插件包含一個或多個鉤子(hooks)方法函數。編寫鉤子(hooks)方法 解釋了如何自己編寫鉤子(hooks)方法函數的基礎知識和細節。pytest
通過調用以下插件的指定掛鉤來實現配置,收集,運行和報告的所有方面:
- 內置插件:從pytest的內部
_pytest
目錄加載。 - 外部插件:通過 setuptools入口點發現的模塊
- conftest.py plugins:在測試目錄中自動發現的模塊
原則上,每個鉤子(hooks)方法調用都是一個1:N
Python函數調用,其中N
是給定規范的已注冊實現函數的數量。所有規范和實現都遵循pytest_
前綴命名約定,使其易于區分和查找。
工具啟動時的插件發現順序
pytest
通過以下方式在工具啟動時加載插件模塊:
通過加載所有內置插件
通過加載通過setuptools入口點注冊的所有插件。
通過預掃描選項的命令行并在實際命令行解析之前加載指定的插件。
-p name
-
通過
conftest.py
命令行調用推斷加載所有文件:- 如果未指定測試路徑,則使用當前dir作為測試路徑
- 如果存在,則加載
conftest.py
并test*/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.py
你PYTHONPATH
或者也可能有其他 文件sys.path
。因此,項目要么放在conftest.py
包范圍內,要么永遠不從conftest.py
文件中導入任何內容, 這是一種很好的做法。
另請參見:pytest import mechanisms和sys.path / PYTHONPATH。
編寫自己的插件
如果你想編寫插件,可以從中復制許多現實示例:
- 自定義集合示例插件:在Yaml文件中指定測試的基本示例
- 內置插件,提供pytest自己的功能
- 許多外部插件提供額外的功能
所有這些插件都實現了鉤子(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"],
)
如果以這種方式安裝包,pytest
將myproject.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
現在,testdir
fixture提供了一個方便的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
這是執行的順序:
- Plugin3的pytest_collection_modifyitems被調用直到屈服點,因為它是一個鉤子(hooks)方法包裝器。
- 調用Plugin1的pytest_collection_modifyitems是因為它標有
tryfirst=True
。 - 調用Plugin2的pytest_collection_modifyitems因為它被標記
trylast=True
(但即使沒有這個標記,它也會在Plugin1之后出現)。 - 插件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())
這具有額外的好處,允許你根據安裝的插件有條件地安裝掛鉤。