官方pytest文檔:Full pytest documentation — pytest documentation
一、pytest以及輔助插件的安裝
1、pytest安裝
pip install pytest
2、輔助插件及介紹
pytest-html(生成htm報告的插件) pytest-xdist (多線程運行的插件) pytest-ordering (改變用例的執行順序的插件) pytest-rerunfailres (失敗用例重跑的插件) allure-pytest (生成美觀自定義的allure報告)pytest test_*.py --alluredir=result + allure serve result
allure generate --alluredir=result -o report/ --clean
pytest-rerunfailures (失敗重跑插件)
pytest-repeat (重復執行用例的插件)
pytest-assume (assume多重斷言,斷言失敗后邊的代碼會繼續執行)
//插件太多,可通過pip install -r pludin.txt一次性下載完成
例如:plugin.txt中內容如下
pytest-html
pytest-xdist
終端運行:pip install -r pugin.txt即可下載這兩個插件
3、安裝插件報錯
1)下面是沒有安裝相應插件報錯,例:沒有安裝allure-pytest插件,運行pytest --alluredir=./temp 會報此錯
ERROR: usage: run.py [options] [file_or_dir] [file_or_dir] [...] run.py: error: unrecognized arguments: --alluredir=./reports/temp/ --clean-alluredir inifile: C:\Users\EDY\PycharmProjects\ProductAuto\pytest.ini rootdir: C:\Users\EDY\PycharmProjects\ProductAuto
二、pytest運行
測試套件默認規則:測試文件必須以test_*開頭,測試類名字必須是Test*開頭,測試方法名字必須是test_*開頭,否則運行時會找不到測試套件
pytest命令的參數:
-vs 輸出詳細信息。輸出調試信息。如: pytest -vs
-n 多線程運行。( 前提安裝插件: pytest-xdist)如: pytest -VS -n=2 --reruns 失敗重跑(前提安裝插件: pytest-rerunfailres )如: pytest --reruns=2 raise Exception()拋出異常,try except解決異常。 -x 出現一個用例失敗則停止測試。如: pytest -x --maxfail 出現幾個失敗才終止,如: pytest --maxfail=2 --html 生成html的測試報告(前提安裝插件: pytest-html) , 如: pytest --html=report/report.html -k 運行測試用例名稱中包含某個字符串的測試用例。 -m 只測試被標記的用例
--strict-markers 未在pytest.ini配置文件注冊的標記mark都會引發報錯
--reruns n (安裝pytest-rerunfailures插件),失敗重跑n次,最大運行次數n pytest --reruns 5
--reruns-delay n )(pytest-rerunfailures插件),pytest --rerun 2 --reruns-delay 5 失敗重跑之間間隔5s
--count 重復執行測試,需安裝pytest-repeat,使用:pytest --count=5或者pytest --count 5
重復執行所有測試用例5次,主要結合-x使用,測試偶發bug,運行直到執行失敗
還有@pytest.mark.repeat(n)可對測試類、方法使用
有兩種執行方式:
1)命令行運行:pytest -vs -x
2)主函數運行:
if __name__ == "__main__": pytest.main(["-vs”,“-x"])
指定運行:
pytest test_*.py //運行指定的test_*.py ,多個py文件分號隔開 pytest testcase/ //運行指定的testcase目錄下所有測試文件 pytest -k "cs” //測試包含有“cs”的測試類和方法 pytest testcase/test_*.py::Test_lei::test_* //可具體指定某個方法
測試執行結果:
- 退出code 0: 收集並成功通過所有測試用例
- 退出code 1: 收集並運行了測試,部分測試用例執行失敗
- 退出code 2: 測試執行被用戶中斷
- 退出code 3: 執行測試中發生內部錯誤
- 退出code 4: pytest命令行使用錯誤
- 退出code 5: 沒有收集到測試用例
全局配置文件pytest.ini,一般建在項目根目錄下
注意:
1)這個配置文件的名稱不能改
2)編碼格式是ANSI,寫入中文后自動變為GB2312 中文簡體
3)pytest.main()會帶上其中配置的參數
pytest.ini配置文件內容解釋,實際中根據需求可選擇性添加配置。
[pytest]
//pytest參數配置 addopts = -VS
//需要執行用例模塊目錄 testpaths =./testcases
//可更改測試文件、類、方法的默認規則,一般不做更改 python_files = test_*.py python_classes = Test* python_functions = test_*
//標記用例,一般用於冒煙測試,以下的smoke是標簽名,”冒煙測試“是描述,使用
markers=
smoke:"冒煙測試"
flow:"流水"
//與@pytest.mark.xfail()的strict一樣,為False,意外pass的結果顯示xpass,為True,則顯示Failed
xfail_strict = False
//控制台日志輸出控制器,為True輸出,為False關閉
log_cli = True
//用來加速收集用例,去掉不要諞歷的目錄
norecursedirs = * (目錄)
斷言類型
1)assert正常斷言
def test_001(self):
assert var==3 //判斷var等於3
assert var in ‘ab’ //判斷字符串‘ab’包含var
assert var is False //判斷var 為假
assert var not True //判斷var不為真
assert var //判斷var為真
2)pytest.raise()異常斷言
def test_002(self): with pytest.raises(ZeroDivisionError): 1/0
3)pytest.warns()警示斷言
暫沒有合適例子
臨時目錄
1)項目運行過程中,總會產生一些臨時文件,pytest提供了tmp_path來創建臨時目錄。
tmp_path是一個pathlib/pathlib2.Path
對象。以下是測試使用方法的示例如:
# test_tmp_path.py文件內容 import os CONTENT = u"content" def test_create_file(tmp_path): d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" p.write_text(CONTENT) assert p.read_text() == CONTENT assert len(list(tmp_path.iterdir())) == 1 assert 0
測試結果如下:除了assert0以外,其他都斷言成功 _____________________________ test_create_file _____________________________ tmpdir = local('PYTEST_TMPDIR/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" assert len(tmpdir.listdir()) == 1 > assert 0 E assert 0
2)tmpdir_factory
四、前置和后置
前置和后置方法如下:
- 模塊級別:setup_module、teardown_module
- 函數級別:setup_function、teardown_function,不在類中的方法
- 類級別:setup_class、teardown_class
- 方法級別:setup_method、teardown_method
- 方法細化級別:setup、teardown
def setup_module(): print("=====整個.py模塊開始前只執行一次:打開瀏覽器=====") def teardown_module(): print("=====整個.py模塊結束后只執行一次:關閉瀏覽器=====") def setup_function(): print("===每個函數級別用例開始前都執行setup_function===") def teardown_function(): print("===每個函數級別用例結束后都執行teardown_function====") def test_one(): print("one") class TestCase(): def setup_class(self): print("====整個測試類開始前只執行一次setup_class====") def teardown_class(self): print("====整個測試類結束后只執行一次teardown_class====") def setup_method(self): print("==類里面每個用例執行前都會執行setup_method==") def teardown_method(self): print("==類里面每個用例結束后都會執行teardown_method==") def setup(self): print("=類里面每個用例執行前都會執行setup=") def teardown(self): print("=類里面每個用例結束后都會執行teardown=") def test_three(self): print("two")
五、fixtrue實現前后置
@pytest.fixtrue(scope="xxx")和yeild實現前置和后置
1)參數scope的五個范圍級別:fuction(默認)、class、module、package、session,實例化順序級別從右至左
2)類調用fixtrue固件只能通過@pytest.mark.usefixtrues("aaa","bbb"),aaa和bbb均為函數名
3)方法能通過參數調用,如def aaa(bbb),aaa為調用函數,bbb為固件函數名,也能使用@pytest.mark.usefixtrues("aaa","bbb")調用
具體效果如下:
//不調用則不顯示 @pytest.fixture() def ttt(): print("fuction-setup4") @pytest.fixture() def aaa(): print("fuction-setup1") @pytest.fixture() def bbb(): print("fuction-setup2") @pytest.fixture(scope="class") def ccc(): print("setup") //調用程序前執行 yield //后面的的方法體相當於后置 print("teardown") //調用程序后執行 //類調用fuction范圍級別固件bbb,相當於類里面的每個方法都調用bbb @pytest.mark.usefixtures("bbb") //類調用class級別固件ccc,只在調用的類前執行一次 @pytest.mark.usefixtures("ccc") class Test_study: def test_ddd(self,aaa): //先bbb輸出,再aaa輸出 print("AAAAAA") @pytest.mark.smoke def test_eee(self): print("BBBBBB") -------------------結果如下-------------------------------- kuadi/test_kuadi_login.py::Test_study::test_ddd setup fuction-setup2 fuction-setup1 AAAAAA PASSED kuadi/test_kuadi_login.py::Test_study::test_eee fuction-setup2 BBBBBB PASSEDteardown
4)其他調用情況
@pytest.fixture(scope="class") def aaa(): print("aaa_start") yield print("aaa_end") @pytest.fixture() def bbb(): print("bbb_start") yield print("bbb_end") @pytest.mark.usefixtures("bbb") def test_fff(): print("fff") class Test_QQQ: @pytest.mark.usefixtures("bbb") def test_ccc(self): print("ccc") def test_ddd(self, aaa): print("ddd") def test_eee(self): print("eee") 結果------------ fds.py::test_fff bbb_start fff PASSEDbbb_end fds.py::Test_QQQ::test_ccc bbb_start ccc PASSEDbbb_end fds.py::Test_QQQ::test_ddd aaa_start ddd PASSED fds.py::Test_QQQ::test_eee eee PASSEDaaa_end
5)yield和addfinalizer函數的使用
//yield配合fixtrue實現前后置方法 @pytest.fixture(scope="class") def ccc(): print("setup") yield print("teardown") //with配合yield,等待測試完成后,自動關閉連接 @pytest.fixture(scope="module") def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: yield smtp_connection //通過后置操作在測試完成后關閉瀏覽器連接 @pytest.fixture(scope="module") def test_addfinalizer(request): # 前置操作setup print("==再次打開瀏覽器==") test = "test_addfinalizer" def fin(): # 后置操作teardown print("==再次關閉瀏覽器==") request.addfinalizer(fin) # 返回前置操作的變量 return test
六、conftest文件
conftest文件注意事項:
- pytest會默認讀取conftest.py里面的所有fixture,所以不用導入fixtrue
- conftest.py 文件名稱是固定的,不能改動
- conftest.py中fixtrue只對同一個package下的所有測試用例生效,其他目錄中引入會報錯not found
- 不同目錄可以有自己的conftest.py,一個項目中可以有多個conftest.py
- 下級目錄的conftest中fixtrue可以覆蓋重寫上級目錄中同名的fixtrue,且就近生效
- 測試用例文件中不需要手動import conftest.py,pytest會自動查找
七、參數化
@pytest.mark.parametrize()
官方文檔:How to parametrize fixtures and test functions — pytest documentation
用法如下:
//測試方法和函數使用
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected
//測試類使用 @pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected def test_weird_simple_case(self, n, expected): assert (n * 1) + 1 == expected
//模塊全局使用 pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected
//mark標記使用 @pytest.mark.parametrize( "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], ) def test_eval(test_input, expected): assert eval(test_input) == expected
//疊加使用 @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): print({x},{y}) 結果按順序:0,2/1,2/0.3/1,3
@pytest.fixtrue()
第一個例子
# content of conftest.py import pytest import smtplib @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection print("finalizing {}".format(smtp_connection)) smtp_connection.close()
//第二個例子
@pytest.fixture(params=[0, 1], ids=["spam", "ham"]) def a(request): return request.param def test_a(a): pass def idfn(fixture_value): if fixture_value == 0: return "eggs" else: return None @pytest.fixture(params=[0, 1], ids=idfn) def b(request): return request.param def test_b(b): pass 結果------------------------------------------------- <Function test_a[spam]> <Function test_a[ham]> <Function test_b[eggs]> <Function test_b[1]>
@pytest.fixtrue帶標記
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)]) def data_set(request): return request.param def test_data(data_set): pass
fixtrue中使用fixtrue:smtp_connection()是第一個用例中fixtrue
class App: def __init__(self, smtp_connection): self.smtp_connection = smtp_connection @pytest.fixture(scope="module") def app(smtp_connection): return App(smtp_connection) def test_smtp_connection_exists(app): assert app.smtp_connection
按fix true實例自動對測試進行分組
@pytest.fixture(scope="module", params=["mod1", "mod2"]) def modarg(request): param = request.param print(" SETUP modarg", param) yield param print(" TEARDOWN modarg", param) @pytest.fixture(scope="function", params=[1, 2]) def otherarg(request): param = request.param print(" SETUP otherarg", param) yield param print(" TEARDOWN otherarg", param) def test_0(otherarg): print(" RUN test0 with otherarg", otherarg) def test_1(modarg): print(" RUN test1 with modarg", modarg) def test_2(otherarg, modarg): print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg)) --------------------------------結果------------------------------- collecting ... collected 8 items test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[mod1-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod1-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 PASSED test_module.py::test_2[mod2-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod2-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2
測試功能不需要直接訪問fixtrue時,如建一個臨時目錄或者文件供測試使用,結合@pytest.markusefixtrues()
# content of conftest.py import os import tempfile import pytest @pytest.fixture def cleandir(): with tempfile.TemporaryDirectory() as newpath: old_cwd = os.getcwd() os.chdir(newpath) yield os.chdir(old_cwd) ------------------------------------ # content of test_setenv.py import os import pytest @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] with open("myfile", "w") as f: f.write("hello") def test_cwd_again_starts_empty(self): assert os.listdir(os.getcwd()) == []
//其他用法 @pytest.mark.usefixtures("cleandir", "anotherfixture") 指定多個fix true def test(): pass //使用pytestmark在測試模塊級別指定夾具的用途 pytestmark = pytest.mark.usefixtures("cleandir") //將項目中所有測試所需的夾具放入ini文件中 [pytest] usefixtures = cleandir
結合@pytest.mark.parametrize()使用,fixtrue作測試數據處理功能,如:factory
@pytest.fixture(scope="function") def input_user(request): user = request.param print("登錄賬戶:%s" % user) return user @pytest.fixture(scope="function") def input_psw(request): psw = request.param print("登錄密碼:%s" % psw) return psw name = ["name1", "name2"] pwd = ["pwd1", "pwd2"] @pytest.mark.parametrize("input_user", name, indirect=True) @pytest.mark.parametrize("input_psw", pwd, indirect=True) def test_more_fixture(input_user, input_psw): print("fixture返回的內容:", input_user, input_psw)
fixtrue會覆蓋同名的fixtrue
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture(params=['one', 'two', 'three']) //最初的參數化fixtrue def parametrized_username(request): return request.param @pytest.fixture //最初的非參數化fixtrue def non_parametrized_username(request): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.fixture //非參數化fixtrue覆蓋參數化fixtrue def parametrized_username(): return 'overridden-username'
//參數化fixtrue覆蓋非參數化fixtrue @pytest.fixture(params=['one', 'two', 'three']) def non_parametrized_username(request): return request.param def test_username(parametrized_username): assert parametrized_username == 'overridden-username' def test_parametrized_username(non_parametrized_username): assert non_parametrized_username in ['one', 'two', 'three'] test_something_else.py # content of tests/test_something_else.py def test_username(parametrized_username): assert parametrized_username in ['one', 'two', 'three'] def test_username(non_parametrized_username): assert non_parametrized_username == 'username'
直接參數化的fixtrue覆蓋非參數化fixtrue
//直接參數化也可以覆蓋fixtrue
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.mark.parametrize('username', ['directly-overridden-username']) def test_username(username): assert username == 'directly-overridden-username'
八、其他裝飾器
官方API Reference — pytest documentation
1、@pytest.mark.xfail()、pytest.xfail
@pytest.mark.xfail(condition,*,reason=none,raises=none,run=True,strict=False)
1)raises:none表示默認匹配所有異常錯誤,值為type(Exception),只能是異常值
2)condition:boolea or str類型,就是有條件執行,只跳過部分特殊用例時使用
3)run:為true,該怎么樣就怎么樣,為False,則該測試套件不會運行,且始終標為xfail
4)reson:原因說明
5)strict:為False,用例通過顯示xpass,用例失敗顯示xfail,為True,用例失敗顯示xfail,用例意外通過則顯示fail
函數、方法、類執行前使用@pytest.mark.xfail(),預期失敗
對方法使用:
//raises參數為空或者與所報的錯誤匹配時結果是xfail,此結果是預期失敗,說明用例通過 @pytest.mark.xfail() def test_002(self): print("賬戶修改") raise Exception //raises參數與所報的錯誤不匹配時結果是failed @pytest.mark.xfail(raises=ZeroDivisionError) def test_004(self): print("賬戶修改") raise Exception --------------------結果------------------------ test_interface.py::Test_kuaidi::test_002 賬戶修改 XFAIL test_interface.py::Test_kuaidi::test_004 賬戶修改 FAILED
對類使用:
@pytest.mark.xfail(raises=ZeroDivisionError) class Test_kuaidi: def test_002(self): //不匹配的用例顯示failed print("賬戶修改") raise Exception def test_004(self): //xpass,意外通過 print("賬戶修改") # raise Exception def test_005(self): //匹配的用例顯示xfail 1/0 ------------------------結果展示--------------- test_interface.py::Test_kuaidi::test_002 賬戶修改 FAILED test_interface.py::Test_kuaidi::test_004 賬戶修改 XPASS test_interface.py::Test_kuaidi::test_005 XFAIL
pytest.xfail(reason=none),主要使用來對已知錯誤和功能缺失使用的,使用例子:
def test_aaa(): pytest.xfail(reason="功能未做") ---------結果------------------------- test_interface.py::test_aaa XFAIL (功能未做)
2、@pytest.mark.skip()、@pytest.mark.skipif()、pytest.skip、pytest.importorskip
都是跳過用例,區別:
@pytest.mark.skip(reason=None):reason跳過的原因,不影響程序運行
@pytest.mark.skipif(condition, reason=None):condition參數的值只能是Boolean或者str,有條件跳過
@pytest.mark.skip使用在測試類、測試方法、測試函數上,則測試類、測試方法、測試函數中所有用例都會跳過
@pytest.mark.skipif使用在測試類、測試方法、測試函數上,condition有值則根據條件跳過,沒有值和@pytest.mark.skip一樣
@pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows") class TestSkipIf(object): def test_function(self): print("不能在window上運行")
pytest.skip(reason=none,allow_module_level=False,msg)
allow_module_level:默認為False,為True則會允許模塊調用,執行時會跳過模塊中剩余的用例
def test_aaa(): pytest.skip("功能未做") //注意,pytest.skip(reason="功能未做")會報錯 結果:test_interface.py::test_aaa SKIPPED (功能未做) pytest.skip(r"功能未做",allow_module_level=True) 結果:跳過所有剩余用例
pytest.importorskip(modname,minversion=none,reason=none)
作用:缺少導入包,跳過用例測試
參數:modname,導入的模塊名;minversion,模塊最小版本號
expy = pytest.importorskip("pytest", minversion="7.0",reason="pytest7.0版本沒有出來") @expy def test_import(): print("test")
結果:會跳過所有該模塊的用例測試
3、@pytest.mark.flaky()
前提安裝pytest-rerunfailures,reruns:重跑次數,reruns_delay:重跑間隔時間
禁止:1、不能和@pytest.fixtrue()一起使用
2、該插件與pytest-xdist的looponfail不兼容
3、與核心--pdb標志不兼容
@pytest.mark.flaky(reruns=2,reruns_delay=5) def test_ddd(self): assert False ---------------------執行結果------------------------- kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN kuadi/test_kuadi_login.py::Test_study::test_ddd FAILED
九、hooks函數
學習地址:Pytest權威教程21-API參考-04-鈎子函數(Hooks) - 韓志超 - 博客園 (cnblogs.com)