第一部分:快速入門
pytest是軟件測試框架,這意味着pytest是命令行工具。它會自動找到你寫的測試,運行測試並報告結果。可編寫插件或安裝第三方來擴展插件。它可以用來測試Python發行版。它很容易與其他工具對接,如持續集成和網頁自動化。
Pytest脫穎而出的原因:
- 簡單
- 易讀
- 用assert來測試失敗,而不是self.assertEqual() 或者self.assertLessThan()
- 可運行unittest或nose測試
1.1,安裝Pytest
$ pip install pytest
1.2,安裝驗證
pytest --version # 會展示當前已安裝版本
1.3,pytest執行原則:
所有的單測文件名應該命名為test_.py或_test.py
在單測文件中,測試類以Test開頭,並且不能帶有 init 方法(注意:定義class時,需要以T開頭,不然pytest是不會去運行該class的)
在單測類中,測試方法和函數應該被命名為test_。
此時,在執行pytest命令時,會自動從當前目錄及子目錄中尋找符合上述約束的測試函數來執行。
結果類型: 以下是測試功能的可能結果: PASSED (.):測試成功。 FAILED (F):測試失敗(或XPASS + strict)。 SKIPPED (s): 測試被跳過。 你可以使用@pytest.mark.skip()或 pytest.mark.skipif()修飾器告訴pytest跳過測試 xfail (x):預期測試失敗。@pytest.mark.xfail() XPASS (X):測試不應該通過。 ERROR (E):錯誤
1.4,pytest 執行第一個函數
Pytest 使用 Python 的assert 進行條件判斷,最簡單的測試函數如:
測試成功的用例:
# test_01.py
import pytest
def test_01(): print('------------>運行test_01') assert (1, 2, 3) == (1, 2, 3)
使用命令 pytest
運行測試函數
pytest 文件路徑/測試文件名
例如:pytest ./test/test_01.py
$ pytest ./test/test01.py ==================================================================== test session starts ===================================================================== platform win32 -- Python 3.7.8, pytest-5.4.3, py-1.10.0, pluggy-0.13.1 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: allure-pytest-2.9.43, assume-2.4.3, html-2.1.1, metadata-1.11.0 collected 1 item test\test01.py . [100%] ===================================================================== 1 passed in 0.05s ======================================================================
注解:
pytest 使用 .
標識測試成功(PASSED
)。
小技巧:
可以使用 -v
選項,顯示測試的詳細信息。
$ pytest ./test/test01.py -v ==================================================================== test session starts ===================================================================== platform win32 -- Python 3.7.8, pytest-5.4.3, py-1.10.0, pluggy-0.13.1 -- c:\python37\python.exe cachedir: .pytest_cache metadata: {'Python': '3.7.8', 'Platform': 'Windows-7-6.1.7601-SP1', 'Packages': {'pytest': '5.4.3', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'allure-py test': '2.9.43', 'assume': '2.4.3', 'html': '2.1.1', 'metadata': '1.11.0'}} rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: allure-pytest-2.9.43, assume-2.4.3, html-2.1.1, metadata-1.11.0 collected 1 item test/test01.py::test_01 PASSED [100%] ===================================================================== 1 passed in 0.05s ======================================================================
測試失敗的用例:
#test_02.py import pytest def test_02(): print('------------>運行test_01') assert (1, 2, 3) == (3, 2, 1)
指令運行:
$pytest ./test/test02.py ==================================================================== test session starts ===================================================================== platform win32 -- Python 3.7.8, pytest-5.4.3, py-1.10.0, pluggy-0.13.1 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: allure-pytest-2.9.43, assume-2.4.3, html-2.1.1, metadata-1.11.0 collected 1 item test\test02.py F [100%] ========================================================================== FAILURES ========================================================================== __________________________________________________________________________ test_02 ___________________________________________________________________________ def test_02(): print('------------>運行test_02') > assert (1, 2, 3) == (3, 2, 1) E AssertionError test\test02.py:12: AssertionError -------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------- ------------>運行test_02 ================================================================== short test summary info =================================================================== FAILED test/test02.py::test_02 - AssertionError ===================================================================== 1 failed in 0.21s ======================================================================
注解:
pytest 使用 F
標識測試失敗(FAILED
)。
pytest 對失敗的測試給出了非常人性化的提示。
1.5,控制執行測試用例
1.5.1,指定測試模塊
pytest test_mod.py
#test_01.py import pytest def test_01py_01(): print('------------>運行test_01py_01') assert (1, 2, 3) == (1, 2, 3) def test_01py_02(): print('------------>運行test_01py_02') assert (1, 2, 3) == (1, 2, 3) def test_01py_03(): print('------------>運行test_01py_03') assert (1, 2, 3) == (1, 2, 3) def test_01py_04(): print('------------>運行test_01py_04') assert (1, 2, 3) == (1, 2, 3)
運行結果
(venv) E:\PycharmScripts\Python36\pytestScripts>pytest ./test/test_01.py ============================================================== test session starts ============================================================== platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 4 items test\test_01.py .... [100%] =============================================================== 4 passed in 0.06s =============================================================== (venv) E:\PycharmScripts\Python36\pytestScripts>pytest ./test/test_01.py -s ============================================================== test session starts ============================================================== platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 4 items test\test_01.py ------------>運行test_01py_01 .------------>運行test_01py_02 .------------>運行test_01py_03 .------------>運行test_01py_04 . =============================================================== 4 passed in 0.05s ===============================================================
1.5.2,指定測試目錄
pytest test/
1.5.3,通過關鍵字表達式過濾執行
pytest -k "MyClass and not method"
這條命令會匹配文件名、類名、方法名匹配表達式的用例,這里這條命令會運行 TestMyClass.test_something, 不會執行 TestMyClass.test_method_simple
案例:
import pytest def test_01py_01(): print('------------>運行test_01py_01') assert (1, 2, 3) == (1, 2, 3) def test_01py_02(): print('------------>運行test_01py_02') assert (1, 2, 3) == (1, 2, 3) def test_01py_03(): print('------------>運行test_01py_03') assert (1, 2, 3) == (1, 2, 3) def test_01py_04(): print('------------>運行test_01py_04') assert (1, 2, 3) == (1, 2, 3)
運行結果;
#pytest ./test/test_01.py -k "test_01py_02 or test_01py_01" (venv) E:\PycharmScripts\Python36\pytestScripts>pytest ./test/ -k "test_01py_02 or test_01py_01" ============================================================== test session starts ============================================================== platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 10 items / 8 deselected / 2 selected test\test_01.py .. [100%] ======================================================== 2 passed, 8 deselected in 0.08s ========================================================
1.5.4,通過 node id 指定測試用例
nodeid由模塊文件名、分隔符、類名、方法名、參數構成,舉例如下:
運行模塊中的指定用例:
pytest test_mod.py::test_func
案例:
import pytest def test_01py_01(): print('------------>運行test_01py_01') assert (1, 2, 3) == (1, 2, 3) def test_01py_02(): print('------------>運行test_01py_02') assert (1, 2, 3) == (1, 2, 3) def test_01py_03(): print('------------>運行test_01py_03') assert (1, 2, 3) == (1, 2, 3) def test_01py_04(): print('------------>運行test_01py_04') assert (1, 2, 3) == (1, 2, 3)
執行結果:
(venv) E:\PycharmScripts\Python36\pytestScripts>pytest ./test/test_01.py::test_01py_01 ============================================================== test session starts ============================================================== platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test\test_01.py . [100%] =============================================================== 1 passed in 0.05s ===============================================================
運行模塊中的指定方法:
pytest test_mod.py::TestClass::test_method
1.5.5,通過標記表達式執行
這條命令會執行被裝飾器 @pytest.mark.slow 裝飾的所有測試用例
pytest -m slow
案例:
import pytest def test_01py_01(): print('------------>運行test_01py_01') assert (1, 2, 3) == (1, 2, 3)
@pytest.mark.slow def test_01py_02(): print('------------>運行test_01py_02') assert (1, 2, 3) == (1, 2, 3) def test_01py_03(): print('------------>運行test_01py_03') assert (1, 2, 3) == (1, 2, 3)
運行結果:
(venv) E:\PycharmScripts\Python36\pytestScripts>pytest ./test/ -m slow -s ============================================================== test session starts ============================================================== platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 10 items / 9 deselected / 1 selected test\test_01.py ------------>運行test_01py_02 . ================================================== 1 passed, 9 deselected, 1 warning in 0.21s ===================================================
1.5.6,多進程運行cases
當cases量很多時,運行時間也會變的很長,如果想縮短腳本運行的時長,就可以用多進程來運行
安裝pytest-xdist:
pip install pytest-xdist
運行模式:
pytest test01.py -n NUM
其中NUM填寫並發的進程數.
1.5.7,重試運行cases
在做接口測試時,有時會遇到503或短時的網絡波動,導致case運行失敗,而這並非是我們期望的結果,此時可以就可以通過重試運行cases的方式來解決。
安裝pytest-rerunfailures:
pip install pytest-rerunfailures
運行模式:
pytest test01.py --reruns NUM
NUM填寫重試的次數。
1.5.8, 顯示print內容
在運行測試腳本時,為了調試或打印一些內容,我們會在代碼中加一些print內容,但是在運行pytest時,這些內容不會顯示出來。如果帶上-s,就可以顯示了
運行模式:
pytest test01.py -s
另外,pytest的多種運行模式是可以疊加執行的,比如說,你想同時運行4個進程,又想打印出print的內容。可以用
pytest test_se.py -s -n 4
第二部分,測試函數
2.1斷言
在 pytest 中,assert 是編寫測試的最基礎工具。如:
import pytest def test_02py_01(): print('------------>運行test_02py_01') assert (1, 2, 3) == (3, 2, 1) def test_02py_02(): print('------------>運行test_02py_02') assert 2 == 2
具體的 assert 語法參考 https://docs.python.org/3/reference/simple_stmts.html?highlight=assert#the-yield-statement
2.2,異常捕捉
在測試過程中,經常需要測試是否如期拋出預期的異常,以確定異常處理模塊生效。在 pytest 中使用 pytest.raises()
進行異常捕獲:
#test_raises.py
import pytest import socket def test_raises(): with pytest.raises(TypeError) as e: s = socket.socket() s.connect('localhost', '6379') exec_msg = e.value.args[0] assert exec_msg == 'port type must be int' if __name__ == '__main__': pytest.main(['-s','test_raises.py'])
運行結果:
E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:\PycharmScripts\Python36\pytestScripts\test\test_raises.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_raises.py . ============================== 1 passed in 0.13s ============================== Process finished with exit code 0
2.3,跳過測試
上一節提到 pytest 使用標記過濾測試函數,所以對於那些尚未開發完成的測試,最好的處理方式就是略過而不執行測試。
按正向的思路,我們只要通過標記指定要測試的就可以解決這個問題;但有時候的處境是我們能進行反向的操作才是最好的解決途徑,即通過標記指定要跳過的測試。
Pytest 使用特定的標記 pytest.mark.skip 完美的解決了,3種加法如下。
@pytest.mark.skip() #1、跳過方法或用例,未備注原因
@pytest.mark.skip(reason='跳過一個方法或一個測試用例') #2、跳過方法或用例,備注了原因
@pytest.mark.skipif(1==1,reason='跳過一個方法或一個測試用例') #3、當條件滿足,跳過方法或用例,備注了原因
案例一:
import pytest @pytest.mark.skip() def test_skip_01(): print("test_skip_01 該案例跳過不用執行,不用備注原因") @pytest.mark.skip(reason="跳過案例備注了原因") def test_skip_02(): print("test_skip_02 該案例跳過不用執行 但是需要備注原因") @pytest.mark.skipif(1==1,reason="跳過案例備注了原因") def test_skip_03(): print("test_skip_03 該案例跳過不用執行 但是需要備注原因") if __name__ == '__main__': pytest.main(['-s','test_skip.py']) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_skip.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 3 items test_skip.py sss ============================= 3 skipped in 0.17s ============================== Process finished with exit code 0
案例二:
# skip跳過類 @pytest.mark.skip() class TestSkip01(object): def test_one(self): assert 1 == 1 def test_two(self): print('test_02') assert 1 == 2 @pytest.mark.skip(reason='跳過Test類,會跳過類中所有方法') class TestSkip02(object): def test_one(self): assert 1 == 1 def test_two(self): print('test_02') assert 1 == 2 @pytest.mark.skipif(1 == 1, reason='跳過Test類,會跳過類中所有方法') class TestSkip03(object): def test_one(self): assert 1 == 1 def test_two(self): print('test_02') assert 1 == 2 @pytest.mark.skipif(1==1,reason='多個條件時,有1個條件滿足就跳過(類)') class TestSkip04(object): @pytest.mark.skipif(1==2, reason='多個條件時,有1個條件滿足就跳過(方法)') def test_one(self): assert 1==2 def test_two(self): print('test_02') # 賦值:myskip=pytest.mark.skipif(1==1,reason='skip賦值給變量,可多處調用') # 調用:@myskip myskip=pytest.mark.skipif(1==1,reason='skip賦值給變量,可多處調用') class TestSkip05(object): @myskip def test_one(self): assert 1==2 def test_two(self): print('test_02') # pytest.skip()方法內跳過 # 除了通過使用標簽的方式,還可以在測試用例中調用pytest.skip()方法來實現跳過,可以選擇傳入msg參數來說明跳過原因;如果想要通過判斷是否跳過,可以寫在if判斷里(_) class TestSkip06(object): def test_one(self): pytest.skip(msg='跳過') assert 1==2 def test_two(self): print('test_02') if __name__ == '__main__': pytest.main(['-rs','test_skip.py']) E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_skip.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 12 items test_skip.py sssssssss.s. [100%] =========================== short test summary info =========================== SKIPPED [2] test_skip.py: unconditional skip SKIPPED [2] test_skip.py: 跳過Test類,會跳過類中所有方法 SKIPPED [1] test_skip.py:49: 跳過Test類,會跳過類中所有方法 SKIPPED [1] test_skip.py:52: 跳過Test類,會跳過類中所有方法 SKIPPED [1] test_skip.py:59: 多個條件時,有1個條件滿足就跳過(類) SKIPPED [1] test_skip.py:62: 多個條件時,有1個條件滿足就跳過(類) SKIPPED [1] test_skip.py:73: skip賦值給變量,可多處調用 SKIPPED [1] test_skip.py:83: 跳過 ================== 2 passed, 10 skipped, 1 warning in 0.18s ===================
注解:
pytest 使用 s
表示測試被跳過(SKIPPED
)。
2.3,遇見錯誤
如果我們事先知道測試函數會執行失敗,但又不想直接跳過,而是希望顯示的提示。
Pytest 使用 pytest.mark.xfail
實現預見錯誤功能
import pytest @pytest.mark.xfail(reason="reason='not supported until v0.2.0'") def test_xfial_01(): print('該案例暫時不執行') assert 1==1 if __name__ == "__main__": pytest.main(['-s', 'test_xfail.py']) E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:\PycharmScripts\Python36\pytestScripts\test\test_xfail.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_xfail.py 該案例暫時不執行 X ============================= 1 xpassed in 0.14s ============================== Process finished with exit code 0
2.4,參數化
當對一個測試函數進行測試時,通常會給函數傳遞多組參數。比如測試賬號登陸,我們需要模擬各種千奇百怪的賬號密碼。
在 pytest 中,我們有更好的解決方法,就是參數化測試,即每組參數都獨立執行一次測試。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)
。
方便測試函數對測試屬於的獲取。 方法: parametrize(argnames, argvalues, indirect=False, ids=None, scope=None) 常用參數: argnames:參數名 argvalues:參數對應值,類型必須為list 當參數為一個時格式:[value] 當參數個數大於一個時,格式為:[(param_value1,param_value2.....),(param_value1,param_value2.....)] 使用方法: @pytest.mark.parametrize(argnames,argvalues)
案例一:
import pytest @pytest.mark.parametrize("a",[1,2,3]) # a參數被賦予兩個值,函數會運行三遍 def test_01(a): # 參數必須和parametrize里面的參數一致 print('測試數據 a=%s' % a) if __name__ == "__main__": pytest.main(['-s','test_params.py']) #執行結果:
E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 3 items test_params.py 測試數據 a=1 .測試數據 a=2 .測試數據 a=3 . ============================== 3 passed in 0.13s ============================== Process finished with exit code 0
案例二:
import pytest #字典類型的數組 @pytest.mark.parametrize('dict_data',[{'name':'python',"age":10,},{'name':'java',"age":15}]) def test_01(dict_data): print('測試數據 dict_data=%s' % dict_data) if __name__ == "__main__": pytest.main(['-s','test_params.py']) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_params.py 測試數據 dict_data={'name': 'python', 'age': 10} .測試數據 dict_data={'name': 'java', 'age': 15} . ============================== 2 passed in 0.16s ============================== Process finished with exit code 0
案例三:
@pytest.mark.parametrize("a,b",[(1,2),(0,3)]) # 參數a,b均被賦予兩個值,函數會運行兩遍 def test_a(a,b): # 參數必須和parametrize里面的參數一致 print("測試數據:a=%d,b=%d" % (a, b)) if __name__ == "__main__": pytest.main(['-s','test_params.py']) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_params.py 測試數據:a=1,b=2 .測試數據:a=0,b=3 . ============================== 2 passed in 0.14s ============================== Process finished with exit code 0
案例四:
# 函數返回值類型示例: def return_test_data(): return [(1,2),(0,3)] @pytest.mark.parametrize("a,b",return_test_data()) # 使用函數返回值的形式傳入參數值 def test_a(a,b): # 參數必須和parametrize里面的參數一致 print("測試數據:a=%d,b=%d" % (a, b)) if __name__ == "__main__": pytest.main(['-s','test_params.py']) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_params.py 測試數據:a=1,b=2 .測試數據:a=0,b=3 . ============================== 2 passed in 0.56s ============================== Process finished with exit code 0
案例五:
import pytest #params數據類型是list @pytest.fixture(params=[1, 2, 3]) def need_data(request): # 傳入參數request 系統封裝參數 print("獲取的參數:%s" % request.param) return request.param # 取列表中單個值,默認的取值方式 def test_a(need_data): print("------->test_a") if __name__ == "__main__": pytest.main(['-s','test_params.py']) #運行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 3 items test_params.py 獲取的參數:1 ------->test_a .獲取的參數:2 ------->test_a .獲取的參數:3 ------->test_a . ============================== 3 passed in 0.17s ============================== Process finished with exit code 0
案例六:
dict1=[ {'a':1,'b':2,'result':3}, {'a':2,'b':2,'result':4} ] @pytest.fixture(params=dict1) def param(request): print('fixture的參數:%s' % request.param) return request.param def test_a(param): print(param['a'],param['b']==param['result']) if __name__ == "__main__": pytest.main(['-s','test_params.py']) #執行結果; E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_params.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_params.py fixture的參數:{'a': 1, 'b': 2, 'result': 3} 1 False .fixture的參數:{'a': 2, 'b': 2, 'result': 4} 2 False . ============================== 2 passed in 0.16s ============================== Process finished with exit code 0
第三部分:固件
3.1, pytest.fixture()
固件(Fixture)是一些函數,pytest 會在執行測試函數之前(或之后)加載運行它們。
我們可以利用固件做任何事情,其中最常見的可能就是數據庫的初始連接和最后關閉操作。
Pytest 使用文件 conftest.py
集中管理固件,conftest.py文件名稱時固定的,pytest會自動識別該文件。放到項目的根目錄下就可以全局調用了,如果放到某個package下,那就在改package內有效。
conftest.py的特點:
有關conftest.py的詳細介紹請查看:https://www.jb51.net/article/207214.htm
- 可以跨.py文件調用,有多個.py文件調用時,可讓conftest.py只調用了一次fixture,或調用多次fixture
- conftest.py與運行的用例要在同一個pakage下,並且有__init__.py文件
- 不需要import導入 conftest.py,pytest用例會自動識別該文件,放到項目的根目錄下就可以全局目錄調用了,如果放到某個package下,那就在該package內有效,可有多個conftest.py
- conftest.py配置腳本名稱是固定的,不能改名稱
- conftest.py文件不能被其他文件導入
- 所有同目錄測試文件運行前都會執行conftest.py文件
Pytest 使用 pytest.fixture()
定義固件,下面是最簡單的固件,只返回北京郵編:
方法:fixture(scope="function", params=None, autouse=False, ids=None, name=None) 常用參數: scope:被標記方法的作用域 function" (default):作用於每個測試方法,每個test都運行一次 "class":作用於整個類,每個class的所有test只運行一次 "module":作用於整個模塊,每個module的所有test只運行一次 "session:作用於整個session(慎用),每個session只運行一次 params:(list類型)提供參數數據,供調用標記方法的函數使用 autouse:是否自動運行,默認為False不運行,設置為True自動運行
ids:每個字符串id的列表,每個字符串對應於params這樣他們就是測試ID的一部分。如果沒有提供ID它們將從params自動生成;是給每一項params參數設置自定義名稱用,意義不大。
name:fixture的名稱。這默認為裝飾函數的名稱。如果fixture在定義它的統一模塊中使用,夾具的功能名稱將被請求夾具的功能arg遮蔽,解決這個問題的一種方法時將裝飾函數命
令"fixture_<fixturename>"然后使用"@pytest.fixture(name='<fixturename>')"。
fixture第一個例子(通過參數引用)
import pytest class Test_ABC: @pytest.fixture() def before(self): print("------->before") def test_a(self,before): # test_a方法傳入了被fixture標識的函數,已變量的形式 print("------->test_a") assert 1 if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_fixture.py ------->before ------->test_a . ============================== 1 passed in 0.17s ============================== Process finished with exit code 0
fixture第二個例子(通過函數引用)
@pytest.fixture() # fixture標記的函數可以應用於測試類外部 def before(): print("------->before") @pytest.mark.usefixtures("before")#1.需要前面標記了before函數,這才可以用,所以需求before函數前面標記@pytest.fixture();2.前面標記了before函數,這不引用的話,執行后不執行before函數 # 比如在接口測試中有需要先登錄的就可以使用這個用法 class Test_ABC: def setup(self): print("------->setup") def test_a(self): print("------->test_a") assert 1 if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_fixture.py ------->setup ------->before ------->test_a . ============================== 1 passed in 0.14s ============================== Process finished with exit code 0
疊加usefixtures
如果一個方法或者一個class用例想要同時調用多個fixture,可以使用@pytest.mark.usefixture()進行疊加。注意疊加順序,先執行的放底層,后執行的放上層。
usefixtures與傳fixture區別
如果fixture有返回值,那么usefixture就無法獲取到返回值,這個是裝飾器usefixture與用例直接傳fixture參數的區別。
當fixture需要用到return出來的參數時,只能講參數名稱直接當參數傳入,不需要用到return出來的參數時,兩種方式都可以。
@pytest.fixture() def test1(): print('開始執行function1') @pytest.fixture() def test2(): print('開始執行function2') @pytest.mark.usefixtures('test1') @pytest.mark.usefixtures('test2') def test_a(): print('---用例a執行---') @pytest.mark.usefixtures('test2') @pytest.mark.usefixtures('test1') class Test_Case: def test_b(self): print('---用例b執行---') def test_c(self): print('---用例c執行---') if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 3 items test_fixture.py 開始執行function2 開始執行function1 ---用例a執行--- .開始執行function1 開始執行function2 ---用例b執行--- .開始執行function1 開始執行function2 ---用例c執行--- . ============================== 3 passed in 0.14s ============================== Process finished with exit code 0
fixture第三個例子(默認設置為自動運行)
@pytest.fixture(autouse=True) # 設置為默認自動運行 def before(): print("------->before") class Test_ABC: def setup(self): print("------->setup") def test_a(self): print("------->test_a") if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_fixture.py ------->before ------->setup ------->test_a . ============================== 1 passed in 0.18s ============================== Process finished with exit code 0
fixture第四個例子(接收fixture返回的參數)
如果用例需要用到多個fixture的返回數據,fixture也可以返回一個元祖,list或字典,然后從里面取出對應數據。
@pytest.fixture() def test1(): user = 'admin' pwd = '123456' print('傳出user,pwd') return (user, pwd) def test_02(test1): u = test1[0] p = test1[1] assert u == 'admin' assert p == '123456' print('元祖形式正確') if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_fixture.py 傳出user,pwd 元祖形式正確 . ============================== 1 passed in 0.15s ============================== Process finished with exit code 0
fixture第五個例子(分開使用多個fixture)
@pytest.fixture() def test_u(): user = 'admin' print('傳出 user') return user @pytest.fixture() def test_p(): pwd = '123456' print('傳出 pwd') return pwd def test_up(test_u, test_p): u = test_u p = test_p print('傳入多個fixture參數') assert u == 'admin' assert p == '123456' if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_fixture.py 傳出 user 傳出 pwd 傳入多個fixture參數 . ============================== 1 passed in 0.13s ============================== Process finished with exit code 0
fixture第六個例子(設置作用域為class)
@pytest.fixture(scope='class',autouse=True) # 作用域設置為class,自動運行 def before(): print("------->before") class Test_ABC: def setup(self): print("------->setup") def test_a(self): print("------->test_a") assert 1 def test_b(self): print("------->test_b") assert 1 if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_fixture.py ------->before ------->setup ------->test_a .------->setup ------->test_b . ============================== 2 passed in 0.15s ============================== Process finished with exit code 0
fixture第七個例子(設置作用域為module)
@pytest.fixture(scope='module') def test1(): b = '男' print('傳出了%s, 且在當前py文件下執行一次!!!' % b) return b def test_3(test1): name = '男' print('找到name') assert test1 == name class Test_Case: def test_4(self, test1): sex = '男' print('找到sex') assert test1 == sex if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果; E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_fixture.py 傳出了男, 且在當前py文件下執行一次!!! 找到name .找到sex . ============================== 2 passed in 0.15s ============================== Process finished with exit code 0
fixture第八個例子(設置作用域為session)
fixture為session級別是可以跨.py模塊調用的,也就是當我們有多個.py文件的用例的時候,如果多個用例只需調用一次fixture,那就可以設置為scope="session",並且寫到 conftest.py文件里;
注意:
conftest.py文件名稱時固定的,pytest會自動識別該文件。放到項目的根目錄下就可以全局調用了,如果放到某個package下,那就只在該package內有效。
一個工程下可以建多個conftest.py的文件,一般在工程根目錄下設置的conftest文件起到全局作用。在不同子目錄下也可以放conftest.py的文件,作用范圍只能在該層級以及以下目錄生效。
- conftest在不同的層級間的作用域不一樣
- conftest是不能跨模塊調用的(這里沒有使用模塊調用)
import pytest # conftest.py @pytest.fixture(scope='session') def test1(): a='admin' print('獲取到%s' % a) return a import pytest # test_01.py def test3(test1): b= '123456' print('找到b') assert test1 == user if __name__ == '__main__': pytest.main(['-s', 'abc.py']) import pytest # test_02.py class Test_Case: def test_4(self, test1): a = 'door' print('找到a') assert test1 == a if __name__ == '__main__': pytest.main(['-s', 'abc1.py']) 如果需要同時執行兩個py文件,可以在cmd的終端中在文件py文件所在目錄下執行命令:pytest -s test_abc.py test_abc.py
fixture第九個例子(更改函數名稱)
@pytest.fixture(name='user_pwd') def test1(): user = 'admin' pwd = '123456' print('傳出user,pwd') return (user, pwd) def test_3(user_pwd): name = user_pwd[0] print('找到name') assert name == 'admin' class Test_Case: def test_4(self, user_pwd): pwd = user_pwd[1] print('找到sex') assert pwd == '123456' if __name__ == '__main__': pytest.main(["-s","test_fixture.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_fixture.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 2 items test_fixture.py 傳出user,pwd 找到name .傳出user,pwd 找到sex . ============================== 2 passed in 0.18s ============================== Process finished with exit code 0
fixture第十個例子(conftest自動執行順序)
#conftest.py @pytest.fixture(scope="session",autouse=True) def bzs_login(zhangsan_info): print('11111') @pytest.fixture(scope="session",autouse=True) def als_login(lisi_info,member_controller_z): print('2222') @pytest.fixture(scope="session") def zhangsan_info(): print('3333') @pytest.fixture(scope="session") def lisi_info(): print('44444') @pytest.fixture(scope="session") def member_controller_z(): print('55555') @pytest.fixture(scope="session") def receiving_z(): print('66666') @pytest.fixture(scope="session") def member_controller_l(): print('777777') @pytest.fixture(scope="session") def receiving_l(): print('8888888')
#test_01.py import pytest def test_yoyo_01(receiving_l): assert 1==1 if __name__ == '__main__': pytest.main(['test_01.py','-s'])
執行順序
由執行結果可以看出當設置為自動執行(autouse=True)的時候,從上到下執行用例,conftest文件文件中的函數,並不是從上到下執行,而是按照首字母順序來執行,即 a(als_login)字母在b(bls_login)字母前面,所以現在執行方法(als_login),如果是手動執行就不會有這個問題,但是自動執行需的考慮,關聯請求中,初始化首字母執行順序問題。同時還發現在傳入參數的名稱,與方法的名字一致時,會現在先運行於參數名稱一致的方法(前提是scope必須是一致如都是session)。
同作用域的函數可以直接調用。如session的函數(scope="session") 可以直接調用作用域是session的函數(scope="session")
作用域是module的函數(scope="module") 可以直接調用作用域是session的函數(scope="session")
3.2,Pytest的setup和teardown
- pytest提供了兩套互相獨立的setup 與 teardown和一對相對自由的setup與teardown
- 模塊級與函數級
模塊級(setup_module/teardown_module) #開始於模塊始末(不在類中)
函數級(setup_function/teardown_function) #只對函數用例生效(不在類中)
- 方法級與類級
方法級(setup_method/teardown_method) #開始於方法始末(在類中)
類級(setup_class/teardown_class) #只在類中前后運行一次(在類中)
- 類里面的(setup/teardown) #運行在調用方法的前后
import pytest def setup_module():#在類中不生效 print("setup_module:整個test_setup.py模塊只執行一次") def teardown_module(): print("teardown_moduel:整個test_setup.py模塊只執行一次") def setup_function(): #只對函數用例生效,不在類中 print("setup_function():每個方法開始前執行") def teardown_function(): print("teardown_function:每個方法結束后執行") def test_01(): print('-----測試用例test_01----') def test_02(): print('-----測試用例test_02----') class Test_MyClass(): def setup_class(self):#只在類中生效 print("setup_class(self):每個類之前執行一次") def teardown_class(self):#只在類中生效 print("teardown_class(self):每個類結束后執行一次") def setup_method(self): #只在類中生效 print("setup_method(self):在類中的每個方法開始之前執行") def teardown_method(self): #只在類中生效 print("setup_method(self):在類中的每個方法開始之前執行") def setup(self): #運行在調用方法的前后 print("setup:每個用例開始前都會執行") def teardown(self): print("teardown:每個用例結束后都會執行") def test_a(self): print("正在執行測試類----test_a") def test_b(self): print("正在執行測試類----test_b") if __name__ == "__main__": pytest.main(["-s", "test_setup.py"]) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_setup.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 4 items test_setup.py setup_module:整個test_setup.py模塊只執行一次 setup_function():每個方法開始前執行 -----測試用例test_01---- .teardown_function:每個方法結束后執行 setup_function():每個方法開始前執行 -----測試用例test_02---- .teardown_function:每個方法結束后執行
setup_class(self):每個類之前執行一次 setup_method(self):在類中的每個方法開始之前執行 正在執行測試類----test_a .setup_method(self):在類中的每個方法開始之前執行 setup_method(self):在類中的每個方法開始之前執行 正在執行測試類----test_b .setup_method(self):在類中的每個方法開始之前執行 teardown_class(self):每個類結束后執行一次 teardown_moduel:整個test_setup.py模塊只執行一次 ============================== 4 passed in 0.20s ============================== Process finished with exit code 0
3.3,預處理和后處理 關鍵字 yield
很多時候需要在測試前進行預處理(如新建數據庫連接),並在測試完成進行清理(關閉數據庫連接)。
當有大量重復的這類操作,最佳實踐是使用固件來自動化所有預處理和后處理。
Pytest 使用 yield
關鍵詞將固件分為兩部分,yield
之前的代碼屬於預處理,會在測試前執行;yield
之后的代碼屬於后處理,將在測試完成后執行。
案例:
import pytest @pytest.fixture() def db(): print('關鍵字yield 之前的代碼屬於預處理,會在測試前執行') user = 'admin' yield user print('關鍵字yield 之后的代碼屬於后處理,將在測試完成后執行') def test_01(db): print('正在執行--------測試用例test_01 接收傳遞參數user=%s-----' % db) if __name__ == '__main__': pytest.main(['-s','test_yield.py']) #執行結果: E:\PycharmScripts\Python36\pytestScripts\venv\Scripts\python.exe E:/PycharmScripts/Python36/pytestScripts/test/test_yield.py ============================= test session starts ============================= platform win32 -- Python 3.7.8, pytest-7.0.1, pluggy-1.0.0 rootdir: E:\PycharmScripts\Python36\pytestScripts\test plugins: forked-1.4.0, rerunfailures-10.2, xdist-2.5.0 collected 1 item test_yield.py 關鍵字yield 之前的代碼屬於預處理,會在測試前執行 正在執行--------測試用例test_01 接收傳遞參數user=admin----- .關鍵字yield 之后的代碼屬於后處理,將在測試完成后執行 ============================== 1 passed in 0.09s ============================== Process finished with exit code 0
3.4 ,yield 與 return的區別
共同點:return和yield都可以用來返回值,在一次性的返回值的場景中return和yield的作用是一樣的;
不同點:如果返回的數據是通過for循環等生成的迭代器類型的數據(如列表,元組),return只能在循環外部一次性返回,yield可以在循環內部逐個元素的返回
案例 return:
class TestReturn(): def get_iterator(self): result_list = [] for j in range(3): print("gen_iterator-%s" % j) result_list.append(j) # return在循環的外部,待變量完全生成后一次性返回 return result_list def call_iterator(self): # 執行下邊這句后result_list直接是完成的結果[0,1,2] result_list = self.get_iterator() for i in result_list: print('call_gen_iterator-%s' % i) if __name__ == '__main__': tr = TestReturn() tr.call_iterator()
執行結果如下,可以看到一次性執行完下層函數,生成完整的迭代器類型返回值result_list,一次性返回給上層函數:
案例 yield:
class TestYeild(): def get_iterator(self): for j in range(3): print("gen_iterator-%s" % j) # yield在循環的內部,逐個元素返回 yield j print('關鍵字yield之后的代碼屬於后處理,將在測試完成后執行') def call_iterator(self): # yield並不是直接返回[0,1,2],執行下邊這句后result_list什么值都沒有 result_list = self.get_iterator() # i每請求一個數據,才會觸發gen_iterator生成一個數據 for i in result_list: print('call_iterator-%s' % i) if __name__ == '__main__': ty = TestYeild() ty.call_iterator() ''' 執行結果: gen_iterator-0 call_iterator-0 關鍵字yield之后的代碼屬於后處理,將在測試完成后執行 gen_iterator-1 call_iterator-1 關鍵字yield之后的代碼屬於后處理,將在測試完成后執行 gen_iterator-2 call_iterator-2 關鍵字yield之后的代碼屬於后處理,將在測試完成后執行 '''
從執行結果上可以看到上下層函數是交替進行的,即上層函數請求迭代一個值下層函數才生成一個值並立即返回這個值
第四部分:Pytest常用插件
4.1 Allure測試報告
- 下載Allure 框架,並配置環境變量
allure是一個命令行工具,需要去github上下載最新版https://github.com/allure-framework/allure2/releases
下載完成后,解壓到自己指定的位置,然后並添加環境變量
- 下載allure-pytest插件
注意:pytest-allure-adaptor 這個插件與 allure-pytest 不能共存
pip install allure-pytest
利用Allure框架生成html報告
allure generate %s -o %s" % (xml_file_path,html_file_path) #指令說明: allure generate:規定指令 xml_file_path:測試報告的原始文件,不能打開成html的報告 -o :輸出:output html_file_path: 生成的Allure報告的路徑 可以打開html的報告
if __name__ == '__main__': xml_file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Report/allure_raw',)) html_file_path = os.path.abspath(os.path.join(xml_file_path, 'html')) pytest.main(['-s', '-q', '--alluredir', xml_file_path]) #利用Allure生產html報告 cmd = "allure generate %s -o %s" % (xml_file_path,html_file_path) os.system(cmd)
未執行Allure指令之前,report目錄會生成一個allure_raw的原始文件,這個只是測試報告的原始文件,不能打開成html的報告
- 執行 Allure指令:"allure generate %s -o %s" % (xml_file_path,html_file_path)后
在 allure 報告首頁 ENVIRONMENT 顯示 'There are no environment variables' 沒有環境變量的配置信息。環境變量配置可以添加報告相關的配置參數,如運行的系統環境,版本號,測試環境,測試人員等基本信息此時需要添加 ENVIRONMENT。
environment 配置文件
方法一:environment.properties 文件
在allure的report根目錄下添加一個 environment.properties 文件
文件里面添加環境配置,格式如下
systemVersion=win10 pythonVersion=3.6.0 allureVersion=2.13.0 baseUrl=http://x x x.xxx.xxx.x:8080 projectName=test author=XXXX email=1234567@qq.com blog=https://www.cnblogs.com
方法二:environment.xml
也可以用 environment.xml 文件,格式如下
<environment> <parameter> <key>Browser</key> <value>Chrome</value> </parameter> <parameter> <key>Browser.Version</key> <value>63.0</value> </parameter> <parameter> <key>Stand</key> <value>Production</value> </parameter> </environment>
Allure無趨勢圖
原因:allure-result文件中沒有history文件夾
解決:將allure-report中的history復制到allure-result
xcopy .\allure-report\history .\allure-results\history /e /Y /I
4.2 多重斷言 pytest-assume
在測試用例中我們往往需要多個斷言,然而使用assert斷言時,只要有一個斷言失敗,后續的斷言就無法再繼續執行。現在,我們可以通過使用pytest-assume插件來解決這個問題,當斷失 敗后,仍會繼續執行后續的斷言。
4.2.1 安裝pytest-assume
pip install purest-assume
assert多重斷言案例:
import pytest def test_assert_01():
print('斷言1')
assert 1 + 5 == 6
print('斷言2')
assert 1 + 6 == 8
print('斷言3')
assert 1 + 7 == 8
print('assert多重斷言完成')
if __name__ == '__main__': pytest.main(['-vs', 'test_assert.py'])
從執行結果可以看到,一旦出現斷言失敗,后面的斷言就無法在繼續執行
pytest-assume 多重斷言
def test_assume_01(): print('斷言1') pytest.assume(1 + 5 == 6) print('斷言2') pytest.assume(1 + 6 == 8) print('斷言3') pytest.assume(1 + 7 == 8) print('assume多重斷言完成') if __name__ == '__main__': pytest.main(['-vs', 'test_assert.py'])
從執行結果中可以看出,使用assert斷言時,斷言失敗不會再執行后續的內容;而使用pytest.assume()斷言時,斷言失敗仍會執行至用例結束。這樣更有利於我們一次性獲取用例執行中全部錯誤信息。
通過上下文管理器with使用pytest-assume
pytest assume無法導入:解決ImportError: cannot import name ‘assume‘ from ‘pytest‘問題https://blog.csdn.net/tianshuiyimo/article/details/116519264
from pytest import assume def test_assume_02(): print('斷言1') with assume: assert 1 + 5 == 6 print('斷言2') with assume: assert 1 + 6 == 8 print('斷言3') with assume: assert 1 + 7 == 8 print('assume多重斷言完成') if __name__ == '__main__': pytest.main(['-vs', 'test_assert.py'])
需要注意的是每個with塊只能有一個斷言,如果一個with下有多個斷言,當第一個斷言失敗的時候,后面的斷言就不會起作用的.
import pytest from pytest import assume def test_assert_03(): with assume: print('斷言1') assert 1 + 5 == 6 print('斷言2') assert 1 + 6 == 8 print('斷言3') assert 1 + 7 == 8 print('assume多重斷言完成') if __name__ == '__main__': pytest.main(['-vs', 'test_assert.py'])
4.3 用例依賴 pytest-dependency
我們在編寫測試案例時,理想的狀態下都是要確保案例之間的獨立性,但是往往部分用例之間確實存在關聯,無法做到徹底獨立,那么我們就可以通過使用插件pytest-dependency設置用例之間的依賴關系。當用例A依賴於用例B時,若用例B執行失敗,則用例A將會自動跳過不執行。如此,就可以避免去執行一個必定會失敗的用例,相當於pytest.mark.skip。
4.3.1 安裝 pytest-dependency
pip install pytest-dependency
使用說明:
首先,在標記被依賴用例時,需要在被依賴的用例上添加裝飾器pytest.mark.dependency(),且被依賴用例需要在關聯用例前執行。也可以給被依賴用例設置別名,通過添加參數name實現。 在關聯的依賴用例上,同樣需要添加裝飾器pytest.mark.dependency(depends=[‘用例名稱’]),與之前不同的是,裝飾器必須要填寫depends參數完成用例的關聯,關聯的被依賴用例存在多個時可以使用“,”隔開。 此外,還可以通過scope參數指定用例依賴的范圍,必須是session、package、module、class這四種類型
案例一:
import pytest class TestMyClass(): # 通過裝飾器@pytest.mark.dependency()標記當前用例為被依賴用例,被依賴用例需要優先關聯用例執行 @pytest.mark.dependency() def test_01(self): print('測試用例test_01,執行失敗') assert 1 == 2 # 通過使用裝飾器關聯被依賴用例,通過depends參數指定用例名稱關聯用例,test_01是自己類下的 @pytest.mark.dependency(depends=['test_01']) def test_02(self): print('測試用test_02,跳過') # 標記被依賴用例時,可以通過name參數指定別名 @pytest.mark.dependency(name='func_03') def test_03(self): print("測試用例03,執行成功!") # 使用depends參數指定定義的別名關聯用例 @pytest.mark.dependency(depends=['func_03']) def test_04(self): print("測試用例04,執行成功!") # depends參數可以關聯多個測試用例,使用“,”分隔即可 @pytest.mark.dependency(depends=['test_01', 'func_03']) def test_05(self): print("測試用例05,跳過") if __name__ == '__main__': pytest.main(['-vs', 'test_dependency.py'])
執行結果:
從執行結果可以看出只有依賴用例執行成功時,當前用例才會被執行,否則會被跳過。依賴多個用例時,只有全部成功,才會執行,否則一樣會跳過。
4.3.2 定義依賴范圍
參考http://t.zoukankan.com/se7enjean-p-13513131.html
案例一:scope='class'
作用於所屬的類,外部類不會被關聯
案例二:scope='module'
不傳遞scope,默認參數是'module',作用於當前文件
只會查找當前文件的符合條件的文件名,類里同名的方法不會被依賴
案例三:scope='package'
作用於當前目錄同級的依賴函數,跨目錄無法找到依賴的函數。
案例四:scope='session'
作用域全局,可跨目錄調用。但被依賴的用例必須先執行,如例子中的test01,否則用例會執行跳過!!!!
4.4 失敗重跑 pytest-rerunfailures
4.4.1安裝pytest-rerunfailures
pip install pytest-rerunfailures
使用說明:
失敗重跑共有兩種使用方式,分別是:通過裝飾器執行和命令行執行。 使用裝飾器時,需要在用例上添加裝飾器pytest.mark.flaky(reruns=重新執行最大次數, reruns_delay=執行間隔時間(單位:秒)),在執行過程中,添加了裝飾器的用例在執行失敗后會按照設置的次數和時間重新執行。 通過在命令行執行時,同樣需要指定"rerun"和"rerun-delay"兩個參數來實現,如:pytest --reruns 重新執行最大次數 --reruns-delay 間隔時間。 注意: 1,reruns是重新執行的最大次數,如果在達到這一數量前用例執行成功,則不會繼續重 跑,判斷用例執行通過;否則執行到最大次數后,用例仍失敗,則判斷用例執行失敗。 2,運行失敗的 fixture 或 setup_class 也將重新執行。 兼容性問題: 不可以和fixture裝飾器一起使用: @pytest.fixture() 該插件與pytest-xdist的 --looponfail 標志不兼容 該插件與核心--pdb標志不兼容
案例一:
import pytest import random # 使用裝飾器設置用例失敗后的重新執行最大次數和每次執行的間隔時間(單位:秒) @pytest.mark.flaky(reruns=3, reruns_delay=1) def test_01(): result = random.choice([False, False, True, False, False]) print(f"result={result}") assert result if __name__ == '__main__': pytest.main(['-vs','test_rerunfailures.py'])
從執行的結果可以看出,當用例斷言失敗后,會重新執行,直到達到設置的最大次數或執行成功為止。
4.5 指定用例執行順序 pytest-ordering
4.5.1 安裝pytest-ordering
pip install pytest-ordering
使用說明;
通過給用例添加裝飾器pytest.mark.run(order=執行順序)設置用例的執行順序。在執行的時候,使用裝飾器pytest.mark.run的用例會優先沒有裝飾器的用例執行,設置了執行順序的用例則按照order參數設置的大小升序執行。
案例:
import pytest def test_01(): print('測是用例test_01') def test_02(): print('測是用例test_02') # 使用裝飾器設置執行順序為2 @pytest.mark.run(order=2) def test_03(): print('測是用例test_03') # 使用裝飾器設置執行順序為1 @pytest.mark.run(order=1) def test_04(): print('測是用例test_04') if __name__ == '__main__': pytest.main(['-vs','test_order.py'])
從執行結過可以看出,優先執行標明了執行順序的用例,並按照order的值由小到大執行。
4.6 分布式運行 pytest-xdist
4.6.1 安裝 pytest-xdist
pip install pytest-xdist
使用說明:
在命令行執行用例時,通過參數-n設置並行啟動的進程數量。除了設置具體的數量外,還可以設置為auto,這種情況下,會依據當前設備的cpu數量執行。 此外,還可以通過–dist參數,設置用例分組,同一個組內的用例會在同一個進程中執行。 –dist=loadscope 同一個module或同一個class下的用例會分配為同一組,按class分組優先於module。 –dist=loadfile 同一個.py文件中的用例會分配為同一組。 分布式執行用例的設計原則(重中之重的重點) 用例之間是獨立的,用例之間沒有依賴關系,用例可以完全獨立運行【獨立運行】 用例執行沒有順序,隨機順序都能正常執行【隨機執行】 每個用例都能重復運行,運行結果不會影響其他用例【不影響其他用例】
案例一:
import pytest from time import sleep @pytest.mark.parametrize('keyword', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']) def test_baidu_search(keyword): sleep(1) print(f'搜索關鍵字{keyword}') assert True @pytest.mark.parametrize('user', ['user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7', 'user8', 'user9', 'user10']) def test_login(user): sleep(1) print(f'用戶{user}登錄成功') assert True if __name__ == '__main__': pytest.main(['-vs','test_xdist.py']) # 不使用pytest-xdist運行 # pytest.main(['-vs', '-n', '2', 'test_xdist.py']) # 使用pytest-xdist運行
不使用pytest-xdist運行執行結果:
使用pytest-xdist運行執行結果:
上方的兩次執行結果中可以看出,使用分布式運行后,用例的運行時間明顯縮短。示例中的用例彼此之間沒有關聯,如果實際使用時用例之間存在依賴關系,可以使用–dist參數為用例分組,確保關聯的用例在同一組內。
第五部分:pytest配置文件pytest.ini詳情
pytest.ini :pyteste的主配置文件,可以改變pytest的默認行為,有很多可配置的選項
5.1 配置項 addopts
更改默認命令行選項
pytest用命令行運行時,有時候需要經常使用到某些參數,又不想重復輸入,這時可以使用pytest.ini文件里的addopts設置
[pytest]
addopts=-vps
-v: pyest -v 說明:可以輸出用例更加相信的執行信息,比如用例所在的文件及用例名稱等
-s: pytest -s 說明:輸入我們用例中的調試信息,比如print打印信息等
-m: pytest -m "標記" 說明:執行待定的測試用例 如@pytest.mark.smoke 執行指令為:pytest -m smoke
-q:pytest -q 說明:簡化控制台的輸出,可以看出信息和結果信息
-k:pytest -k "關鍵字" 說明:執行用例包含“關鍵字”的用例