目錄:
- 安裝及入門
- 使用和調(diào)用方法
- 原有TestSuite使用方法
- 斷言的編寫和報告
- Pytest fixtures:清晰 模塊化 易擴展
- 使用Marks標(biāo)記測試用例
- Monkeypatching/對模塊和環(huán)境進行Mock
- 使用tmp目錄和文件
- 捕獲stdout及stderr輸出
- 捕獲警告信息
- 模塊及測試文件中集成doctest測試
- skip及xfail: 處理不能成功的測試用例
- Fixture方法及測試用例的參數(shù)化
- 緩存: 使用跨執(zhí)行狀態(tài)
- unittest.TestCase支持
- 運行Nose用例
- 經(jīng)典xUnit風(fēng)格的setup/teardown
- 安裝和使用插件
- 插件編寫
- 編寫鉤子(hook)方法
- 運行日志
- API參考
- 優(yōu)質(zhì)集成實踐
- 片狀測試
- Pytest導(dǎo)入機制及sys.path/PYTHONPATH
- 配置選項
- 示例及自定義技巧
- Bash自動補全設(shè)置
插件編寫
很容易為你自己的項目實現(xiàn)本地conftest插件或可以在許多項目中使用的可安裝的插件,包括第三方項目。如果你只想使用但不能編寫插件邑退,請參閱安裝和使用插件。
插件包含一個或多個鉤子(hooks)方法函數(shù)鸠信。編寫鉤子(hooks)方法 解釋了如何自己編寫鉤子(hooks)方法函數(shù)的基礎(chǔ)知識和細節(jié)。pytest
通過調(diào)用以下插件的指定掛鉤來實現(xiàn)配置屈暗,收集灸蟆,運行和報告的所有方面:
- 內(nèi)置插件:從pytest的內(nèi)部
_pytest
目錄加載。 - 外部插件:通過 setuptools入口點發(fā)現(xiàn)的模塊
- conftest.py plugins:在測試目錄中自動發(fā)現(xiàn)的模塊
原則上发皿,每個鉤子(hooks)方法調(diào)用都是一個1:N
Python函數(shù)調(diào)用崔慧,其中N
是給定規(guī)范的已注冊實現(xiàn)函數(shù)的數(shù)量。所有規(guī)范和實現(xiàn)都遵循pytest_
前綴命名約定穴墅,使其易于區(qū)分和查找惶室。
工具啟動時的插件發(fā)現(xiàn)順序
pytest
通過以下方式在工具啟動時加載插件模塊:
通過加載所有內(nèi)置插件
通過加載通過setuptools入口點注冊的所有插件。
通過預(yù)掃描選項的命令行并在實際命令行解析之前加載指定的插件玄货。
-p name
-
通過
conftest.py
命令行調(diào)用推斷加載所有文件:- 如果未指定測試路徑皇钞,則使用當(dāng)前dir作為測試路徑
- 如果存在,則加載
conftest.py
并test*/conftest.py
相對于第一個測試路徑的目錄部分松捉。
請注意夹界,pytest
conftest.py
在工具啟動時沒有在更深的嵌套子目錄中找到文件。將conftest.py
文件保存在頂級測試或項目根目錄中通常是個好主意隘世。 通過遞歸加載文件中
pytest_plugins
變量指定的所有插件conftest.py
conftest.py:本地每目錄插件
本地conftest.py
插件包含特定于目錄的鉤子(hooks)方法實現(xiàn)可柿。Hook Session和測試運行活動將調(diào)用conftest.py
靠近文件系統(tǒng)根目錄的文件中定義的所有掛鉤。實現(xiàn)pytest_runtest_setup
鉤子(hooks)方法的示例丙者, 以便在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”可能不明確,因為conftest.py
你PYTHONPATH
或者也可能有其他 文件sys.path
械媒。因此目锭,項目要么放在conftest.py
包范圍內(nèi),要么永遠不從conftest.py
文件中導(dǎo)入任何內(nèi)容纷捞, 這是一種很好的做法痢虹。
另請參見:pytest import mechanisms和sys.path / PYTHONPATH。
編寫自己的插件
如果你想編寫插件主儡,可以從中復(fù)制許多現(xiàn)實示例:
- 自定義集合示例插件:在Yaml文件中指定測試的基本示例
- 內(nèi)置插件世分,提供pytest自己的功能
- 許多外部插件提供額外的功能
所有這些插件都實現(xiàn)了鉤子(hooks)方法和/或固定裝置 以擴展和添加功能。
注意
請務(wù)必查看優(yōu)秀 的cookiecutter-pytest-plugin 項目缀辩,該項目是 用于創(chuàng)作插件的cookiecutter模板。
該模板提供了一個很好的起點踪央,包括一個工作插件臀玄,使用tox運行的測試,一個全面的README文件以及一個預(yù)先配置的入口點畅蹂。
另外考慮將你的插件貢獻給pytest-dev 一旦它擁有一些非自己的快樂用戶健无。
使你的插件可以被他人安裝
如果你想讓你的插件在外部可用,你可以為你的發(fā)行版定義一個所謂的入口點液斜,以便pytest
找到你的插件模塊累贤。入口點是setuptools提供的功能叠穆。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
是使用普通的斷言語句以及斷言失敗時表達式的詳細內(nèi)省嚷硫。這是由“斷言重寫”提供的,它在編譯為字節(jié)碼之前修改了解析的AST始鱼。這是通過一個完成的PEP 302導(dǎo)入掛鉤仔掸,在pytest
啟動時及早安裝 ,并在導(dǎo)入模塊時執(zhí)行此重寫医清。但是起暮,由于我們不想測試不同的字節(jié)碼,因此你將在生產(chǎn)中運行此掛鉤僅重寫測試模塊本身以及作為插件一部分的任何模塊会烙。任何其他導(dǎo)入的模塊都不會被重寫负懦,并且會發(fā)生正常的斷言行為。
如果你在其他模塊中有斷言助手持搜,你需要啟用斷言重寫密似,你需要pytest
在導(dǎo)入之前明確要求重寫這個模塊。
注冊一個或多個要在導(dǎo)入時重寫的模塊名稱葫盼。
此函數(shù)將確保此模塊或程序包內(nèi)的所有模塊將重寫其assert語句残腌。因此,你應(yīng)確保在實際導(dǎo)入模塊之前調(diào)用此方法贫导,如果你是使用包的插件抛猫,則通常在init.py中調(diào)用。
<colgroup><col class="field-name" style="hyphens: manual;"><col class="field-body"></colgroup>
| 舉: | TypeError - 如果給定的模塊名稱不是字符串孩灯。 |
當(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
被重寫讥巡。如果輔助模塊還包含需要重寫的斷言語句掀亩,則需要在導(dǎo)入之前將其標(biāo)記為這樣。通過將其標(biāo)記為在__init__.py
模塊內(nèi)部進行重寫欢顷,這是最簡單的槽棍,當(dāng)導(dǎo)入包中的 模塊時,將始終首先導(dǎo)入該模塊。這種方式plugin.py
仍然可以helper.py
正常導(dǎo)入炼七。然后缆巧,內(nèi)容 pytest_foo/__init__.py
將需要如下所示:
import pytest
pytest.register_assert_rewrite("pytest_foo.helper")
在測試模塊或conftest文件中要求/加載插件
你可以在測試模塊或這樣的conftest.py
文件中要求插件:
pytest_plugins = ["name1", "name2"]
加載測試模塊或conftest插件時,也會加載指定的插件豌拙。任何模塊都可以作為插件祝福陕悬,包括內(nèi)部應(yīng)用程序模塊:
pytest_plugins = "myapp.testsupport.myplugin"
pytest_plugins
變量是遞歸處理的,所以請注意姆蘸,在上面的示例中墩莫,如果myapp.testsupport.myplugin
也聲明pytest_plugins
,變量的內(nèi)容也將作為插件加載逞敷,依此類推狂秦。
注意
pytest_plugins
不建議使用非根conftest.py
文件中使用變量的 插件劫笙。
這很重要罐监,因為conftest.py
文件實現(xiàn)了每個目錄的鉤子(hooks)方法實現(xiàn),但是一旦導(dǎo)入了插件张峰,它就會影響整個目錄樹牛柒。為了避免混淆堪簿,不推薦pytest_plugins
在任何conftest.py
不在測試根目錄中的文件中進行定義 ,并將發(fā)出警告皮壁。
這種機制使得在應(yīng)用程序甚至外部應(yīng)用程序中共享裝置變得容易椭更,而無需使用setuptools
入口點技術(shù)創(chuàng)建外部插件。
導(dǎo)入的插件pytest_plugins
也會自動標(biāo)記為斷言重寫(請參閱參考資料pytest.register_assert_rewrite()
)蛾魄。但是虑瀑,為了使其具有任何效果,必須不必導(dǎo)入模塊; 如果在pytest_plugins
處理語句時已經(jīng)導(dǎo)入它 滴须,則會產(chǎn)生警告舌狗,并且不會重寫插件內(nèi)的斷言。要解決此問題扔水,你可以pytest.register_assert_rewrite()
在導(dǎo)入模塊之前自行調(diào)用痛侍,也可以安排代碼以延遲導(dǎo)入,直到注冊插件為止魔市。
按名稱訪問另一個插件
如果一個插件想要與另一個插件的代碼協(xié)作主届,它可以通過插件管理器獲得一個引用,如下所示:
plugin = config.pluginmanager.get_plugin("name_of_plugin")
如果要查看現(xiàn)有插件的名稱待德,請使用該--trace-config
選項岂膳。
測試插件
pytest附帶一個名為的插件pytester
,可幫助你為插件代碼編寫測試磅网。默認情況下,該插件處于禁用狀態(tài)筷屡,因此你必須先啟用它涧偷,然后才能使用它簸喂。
你可以通過conftest.py
將以下行添加到測試目錄中的文件來執(zhí)行此操作:
# content of conftest.py
pytest_plugins = ["pytester"]
或者,你可以使用命令行選項調(diào)用pytest 燎潮。-p pytester
這將允許你使用testdir
fixture來測試你的插件代碼喻鳄。
讓我們用一個例子演示你可以用插件做什么。想象一下确封,我們開發(fā)了一個插件除呵,它提供了一個hello
產(chǎn)生函數(shù)的fixture ,我們可以用一個可選參數(shù)調(diào)用這個函數(shù)爪喘。如果我們不提供值或者我們提供字符串值颜曾,它將返回字符串值。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
現(xiàn)在秉剑,testdir
fixture提供了一個方便的API來創(chuàng)建臨時 conftest.py
文件和測試文件泛豪。它還允許我們運行測試并返回一個結(jié)果對象,通過它我們可以斷言測試的結(jié)果侦鹏。
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之前復(fù)制示例文件夾的示例
# 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 ===================
有關(guān)runpytest()
返回的結(jié)果對象及其提供的方法的更多信息,請查看RunResult
文檔略水。
編寫鉤子(hooks)方法函數(shù)
鉤子(hooks)方法函數(shù)驗證和執(zhí)行
pytest為任何給定的鉤子(hooks)方法規(guī)范調(diào)用已注冊插件的鉤子(hooks)方法函數(shù)价卤。讓我們看一下鉤子(hooks)方法的典型鉤子(hooks)方法函數(shù),pytest在收集完所有測試項目后調(diào)用渊涝。pytest_collection_modifyitems(session, config,items)
當(dāng)我們pytest_collection_modifyitems
在插件中實現(xiàn)一個函數(shù)時慎璧,pytest將在注冊期間驗證你是否使用了與規(guī)范匹配的參數(shù)名稱,如果沒有則拯救驶赏。
讓我們看一下可能的實現(xiàn):
def pytest_collection_modifyitems(config, items):
# called after collection is completed
# you can modify the ``items`` list
...
這里炸卑,pytest
將傳入config
(pytest配置對象)和items
(收集的測試項列表),但不會傳入session
參數(shù)煤傍,因為我們沒有在函數(shù)簽名中列出它盖文。這種動態(tài)的“修剪”參數(shù)允許pytest
“未來兼容”:我們可以引入新的鉤子(hooks)方法命名參數(shù)而不破壞現(xiàn)有鉤子(hooks)方法實現(xiàn)的簽名。這是pytest插件的一般長期兼容性的原因之一蚯姆。
請注意五续,除了pytest_runtest_*
不允許引發(fā)異常之外的鉤子(hooks)方法函數(shù)。這樣做會打破pytest運行龄恋。
firstresult:首先停止非無結(jié)果
大多數(shù)對pytest
鉤子(hooks)方法的調(diào)用都會產(chǎn)生一個結(jié)果列表疙驾,其中包含被調(diào)用鉤子(hooks)方法函數(shù)的所有非None結(jié)果。
一些鉤子(hooks)方法規(guī)范使用該firstresult=True
選項郭毕,以便鉤子(hooks)方法調(diào)用僅執(zhí)行它碎,直到N個注冊函數(shù)中的第一個返回非None結(jié)果,然后將其作為整個鉤子(hooks)方法調(diào)用的結(jié)果。在這種情況下扳肛,不會調(diào)用其余的鉤子(hooks)方法函數(shù)傻挂。
hookwrapper:在其他鉤子(hooks)方法周圍執(zhí)行
版本2.7中的新功能。
pytest插件可以實現(xiàn)鉤子(hooks)方法包裝器挖息,它包裝其他鉤子(hooks)方法實現(xiàn)的執(zhí)行金拒。鉤子(hooks)方法包裝器是一個生成器函數(shù),它只產(chǎn)生一次套腹。當(dāng)pytest調(diào)用鉤子(hooks)方法時绪抛,它首先執(zhí)行鉤子(hooks)方法包裝器并傳遞與常規(guī)鉤子(hooks)方法相同的參數(shù)。
在鉤子(hooks)方法包裝器的屈服點电禀,pytest將執(zhí)行下一個鉤子(hooks)方法實現(xiàn)幢码,并以Result
封裝結(jié)果或異常信息的實例的形式將其結(jié)果返回到屈服點。因此鞭呕,屈服點本身通常不會引發(fā)異常(除非存在錯誤)蛤育。
以下是鉤子(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)方法包裝器本身不返回結(jié)果葫松,它們只是圍繞實際的鉤子(hooks)方法實現(xiàn)執(zhí)行跟蹤或其他副作用瓦糕。如果底層鉤子(hooks)方法的結(jié)果是一個可變對象,它們可能會修改該結(jié)果腋么,但最好避免它咕娄。
有關(guān)更多信息,請參閱插件文檔珊擂。
鉤子(hooks)方法函數(shù)排序/調(diào)用示例
對于任何給定的鉤子(hooks)方法規(guī)范圣勒,可能存在多個實現(xiàn),因此我們通常將hook
執(zhí)行視為 1:N
函數(shù)調(diào)用摧扇,其中N
是已注冊函數(shù)的數(shù)量圣贸。有一些方法可以影響鉤子(hooks)方法實現(xiàn)是在其他人之前還是之后,即在N
-sized函數(shù)列表中的位置:
# 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
這是執(zhí)行的順序:
- Plugin3的pytest_collection_modifyitems被調(diào)用直到屈服點扛稽,因為它是一個鉤子(hooks)方法包裝器吁峻。
- 調(diào)用Plugin1的pytest_collection_modifyitems是因為它標(biāo)有
tryfirst=True
。 - 調(diào)用Plugin2的pytest_collection_modifyitems因為它被標(biāo)記
trylast=True
(但即使沒有這個標(biāo)記在张,它也會在Plugin1之后出現(xiàn))用含。 - 插件3的pytest_collection_modifyitems然后在屈服點之后執(zhí)行代碼。yield接收一個
Result
實例帮匾,該實例封裝了調(diào)用非包裝器的結(jié)果啄骇。包裝不得修改結(jié)果。
這是可能的使用tryfirst
瘟斜,并trylast
結(jié)合還 hookwrapper=True
處于這種情況下缸夹,它會影響彼此之間hookwrappers的排序痪寻。
聲明新鉤子(hooks)方法
插件和conftest.py
文件可以聲明新鉤子(hooks)方法,然后可以由其他插件實現(xiàn)明未,以便改變行為或與新插件交互:
在插件注冊時調(diào)用槽华,允許通過調(diào)用添加新的掛鉤 。pluginmanager.add_hookspecs(module_or_class, prefix)
參數(shù): | pluginmanager(_pytest.config.PytestPluginManager) - pytest插件管理器
注意:
這個鉤子(hooks)方法與之不相容hookwrapper=True
趟妥。
鉤子(hooks)方法通常被聲明為do-nothing函數(shù),它們只包含描述何時調(diào)用鉤子(hooks)方法以及期望返回值的文檔佣蓉。
有關(guān)示例披摄,請參閱xdist中的newhooks.py。
可選擇使用第三方插件的鉤子(hooks)方法
由于標(biāo)準(zhǔn)的驗證機制勇凭,如上所述使用插件中的新鉤子(hooks)方法可能有點棘手:如果你依賴未安裝的插件疚膊,驗證將失敗并且錯誤消息對你的用戶沒有多大意義。
一種方法是將鉤子(hooks)方法實現(xiàn)推遲到新的插件虾标,而不是直接在插件模塊中聲明鉤子(hooks)方法函數(shù)寓盗,例如:
# 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())
這具有額外的好處,允許你根據(jù)安裝的插件有條件地安裝掛鉤璧函。