快速入門
pytest是Python的單元測試框架,同自帶的unittest框架類似,但pytest框架使用起來更簡潔,效率更高。
pytest特點
- 入門簡單易上手,文檔支持較好。
- 支持單元測試和功能測試。
- 支持參數化。
- 可以跳過指定用例,或對某些預期失敗的case標記成失敗。
- 支持重復執行失敗的case。
- 支持運行由unittest編寫的測試用例。
- 有很多第三方插件,並且可自定義擴展。
- 方便和支持集成工具進行集成。
安裝
pip install pytest
測試
C:\Users\Anthony>pytest --version This is pytest version 5.2.2, imported from c:\python36\lib\site-packages\pytest.py
在測試之前要做的准備
演示腳本處於這樣一個的目錄中:
M:\py_tests\ # 我的是M盤的 py_tests 目錄,所有操作都在 py_tests 目錄內完成 ├─scripts │ ├─test_case_dir1 │ │ ├─test_case_02.py # 用例腳本文件 │ │ └─__init__.py │ ├─test_allure_case.py # 腳本文件 │ ├─test_case_01.py # 腳本文件 │ └─__init__.py ├─report │ ├─report.html # pytest-html生成的用例報告 │ ├─assets # allure的依賴目錄 │ ├─result # allure生成的用例數據 │ └─allure_html # allure生成的用例報告目錄 | └─index.html # allure生成的最終的html類型的測試報告 ├─case_set.py ├─demo0.py # 用例腳本文件 ├─demo1.py # 用例腳本文件 ├─pytest.ini # 配置文件 └─__init__.py
簡單示例
demo1.py
:

import pytest def test_case01(): print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(): print('執行用例02.......') assert 1 # 斷言成功 def custom_case03(): print('執行用例03.......') assert 1 # 斷言成功 if __name__ == '__main__': pytest.main(["-s", "demo1.py"]) # pytest.main("-s demo1.py")
上例中,在執行(就像Python解釋器執行普通的Python腳本一樣)測試用例的時候,pytest.main(["-s", "demo1.py"])
中的傳參需要是一個元組或者列表(我的pytest是5.2.2版本),之前的版本可能需要這么調用pytest.main("-s demo1.py")
,傳的參數是str的形式,至於你使用哪種,取決於報不報錯:
TypeError: `args` parameter expected to be a list or tuple of strings, got: '-s demo1.py' (type: <class 'str'>)
遇到上述報錯,就是參數需要一個列表或者元組的形式,而我們使用的是str形式。
上述代碼正確的執行結果是這樣的:

===================================================== test session starts ====================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 執行用例01....... F執行用例02....... . =========================================================== FAILURES =========================================================== _________________________________________________________ test_case01 __________________________________________________________ def test_case01(): print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:11: AssertionError ================================================= 1 failed, 1 passed in 0.13s ==================================================
大致的信息就是告訴我們:
collected 2 items
:本次執行中,收集了2個用例。- 完了開始執行用例,
.
表示執行成功,F
表示執行失敗。 - 腳本中的第一個用例執行失敗;第二個用例執行成功;但是第三個也就是
custom_case03
並沒有執行,由此我們知道,pytest只識別以test_
開頭的用例。
pytest.main(["-s", "demo1.py"])參數說明
-s
,表示輸出用例執行的詳細結果。demo1.py
是要執行的腳本名稱。
除了上述的函數這種寫法,也可以有用例類的寫法:

import pytest class TestCase(object): def test_case01(self): """ 用例 01 """ print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(slef): """ 用例 02 """ print('執行用例02.......') assert 1 # 斷言成功 if __name__ == '__main__': pytest.main(["-s", "demo1.py"])
用法跟unittest差不多,類名要以Test
開頭,並且其中的用例方法也要以test
開頭,然后執行也一樣。
執行結果:

M:\py_tests>python demo1.py ========================================================== test session starts =========================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 執行用例01....... F執行用例02....... . ================================================================ FAILURES ================================================================ __________________________________________________________ TestCase.test_case01 __________________________________________________________ self = <demo1.TestCase object at 0x03DD6110> def test_case01(self): """ 用例 01 """ print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:49: AssertionError ====================================================== 1 failed, 1 passed in 0.12s =======================================================
那么,這個時候可能會問,我記得unittest中有setup和teardown的方法,難道pytest中沒有嘛?你怎么提都不提?穩住,答案是有的。
接下來,來研究一下pytest中的setup和teardown的用法。
setup和teardown
我們知道,在unittest中,setup和teardown可以在每個用例前后執行,也可以在所有的用例集執行前后執行。那么在pytest中,有以下幾種情況:
- 模塊級別,也就是在整個測試腳本文件中的用例集開始前后,對應的是:
- setup_module
- teardown_module
- 類級別,在類中的所有用例集執行前后,對應的是:
- setup_class
- teardown_class
- 在類中呢,也可以在進一步划分,在每一個方法執行前后,對應:
- setup_method
- teardown_methd
- 函數級別,在用例函數之前后,對應:
- setup_function
- teardown_function
來一一看看各自的用法。
模塊級別setup_module/teardown_module

import pytest def setup_module(): """ 模塊級別的 setup,在該腳本內所有用例集執行之前觸發執行 """ print('模塊級別的 setup.....') def test_case01(): print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(): print('執行用例02.......') assert 1 # 斷言成功 def teardown_module(): """ 模塊級別的 teardown,在該腳本內所有用例集執行之后觸發執行 """ print('模塊級別的 teardown.....') if __name__ == '__main__': pytest.main(["-s", "demo1.py"])
執行結果:

M:\py_tests>python demo1.py ========================================================== test session starts =========================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 模塊級別的 setup..... 執行用例01....... F執行用例02....... .模塊級別的 teardown..... ================================================================ FAILURES ================================================================ ______________________________________________________________ test_case01 _______________________________________________________________ def test_case01(): print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:16: AssertionError ====================================================== 1 failed, 1 passed in 0.12s =======================================================
類級別的setup_class/teardown_class

import pytest class TestCase(object): def setup_class(self): """ 類級別的 setup,在該類中內用例集執行之前觸發執行 """ print('類級別的 setup.....') def teardown_class(self): """ 類級別的 teardown,在該類中內用例集執行之后觸發執行 """ print('類級別的 teardown.....') def test_case01(self): """ 用例 01 """ print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(slef): """ 用例 02 """ print('執行用例02.......') assert 1 # 斷言成功 if __name__ == '__main__': pytest.main(["-s", "demo1.py"])
執行結果:

M:\py_tests>python demo1.py ========================================================== test session starts =========================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 類級別的 setup..... 執行用例01....... F執行用例02....... .類級別的 teardown..... ================================================================ FAILURES ================================================================ __________________________________________________________ TestCase.test_case01 __________________________________________________________ self = <demo1.TestCase object at 0x0363F710> def test_case01(self): """ 用例 01 """ print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:53: AssertionError ====================================================== 1 failed, 1 passed in 0.10s =======================================================
類中方法級別的setup_method/teardown_method

import pytest class TestCase(object): def setup_method(self): """ 類中方法級別的 setup,在該類中內每個用例執行之前觸發執行 """ print('類中方法級別的 setup.....') def teardown_method(self): """ 類中方法級別的 teardown,在該類中內每個用例執行之后觸發執行 """ print('類中方法級別的 teardown.....') def test_case01(self): """ 用例 01 """ print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(slef): """ 用例 02 """ print('執行用例02.......') assert 1 # 斷言成功 if __name__ == '__main__': pytest.main(["-s", "demo1.py"])
執行結果:

M:\py_tests>python demo1.py ========================================================== test session starts =========================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 類中方法級別的 setup..... 執行用例01....... F類中方法級別的 teardown..... 類中方法級別的 setup..... 執行用例02....... .類中方法級別的 teardown..... ================================================================ FAILURES ================================================================ __________________________________________________________ TestCase.test_case01 __________________________________________________________ self = <demo1.TestCase object at 0x042BA2D0> def test_case01(self): """ 用例 01 """ print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:49: AssertionError ====================================================== 1 failed, 1 passed in 0.42s =======================================================
函數級別的setup_function/teardown_function

import pytest def setup_function(): """ 函數級別的 setup,在該腳本內每個用例函數執行之前觸發執行 """ print('函數級別的 setup.....') def test_case01(): print('執行用例01.......') assert 0 # 斷言失敗 def test_case02(): print('執行用例02.......') assert 1 # 斷言成功 def teardown_function(): """ 函數級別的 teardown,在該腳本內每個用例函數執行之后觸發執行 """ print('函數級別的 teardown.....') if __name__ == '__main__': pytest.main(["-s", "demo1.py"])
執行結果:

M:\py_tests>python demo1.py ========================================================== test session starts =========================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests collected 2 items demo1.py 函數級別的 setup..... 執行用例01....... F函數級別的 teardown..... 函數級別的 setup..... 執行用例02....... .函數級別的 teardown..... ================================================================ FAILURES ================================================================ ______________________________________________________________ test_case01 _______________________________________________________________ def test_case01(): print('執行用例01.......') > assert 0 # 斷言失敗 E assert 0 demo1.py:16: AssertionError ====================================================== 1 failed, 1 passed in 0.11s ======================================================= 小結
小結
- 在類中,不需要
__init__
方法。 - 測試類的類名必須以
Test
開頭。 - 類中的測試方法編寫規則跟函數一致。
配置文件
該腳本有多種運行方式,如果處於PyCharm環境,可以使用右鍵或者點擊運行按鈕運行,也就是在pytest中的主函數中運行:

if __name__ == '__main__': pytest.main(["-s", "demo1.py"]) # 就是調用的 pytest 的 main 函數
也可以在命令行中運行:
M:\py_tests>python demo1.py
這種方式,跟使用Python解釋器執行Python腳本沒有什么兩樣。也可以如下面這么執行:
M:\py_tests>pytest -s demo1.py
當然,還有一種是使用配置文件運行,來看看怎么用。
在項目的根目錄下,我們可以建立一個pytest.ini
文件,在這個文件中,我們可以實現相關的配置:
[pytest] addopts = -s -v testpaths = ./scripts python_files = test_*.py python_classes = Test* python_functions = test_*
注意,配置文件中不許有中文
那這個配置文件中的各項都是什么意思呢?
首先,pytest.ini
文件必須位於項目的根目錄,而且也必須叫做pytest.ini
。
其他的參數:
-
addopts
可以搭配相關的參數,比如-s
。多個參數以空格分割,其他參數后續用到再說。-s
,在運行測試腳本時,為了調試或打印一些內容,我們會在代碼中加一些print內容,但是在運行pytest時,這些內容不會顯示出來。如果帶上-s,就可以顯示了。-v
,使輸出結果更加詳細。
-
testpaths
配置測試用例的目錄,- 因為我們用例可能分布在不同的目錄或文件中,那么這個
scripts
就是我們所有文件或者目錄的頂層目錄。其內的子文件或者子目錄都要以test_
開頭,pytest才能識別到。 - 另外,上面這么寫,是從一個總目錄下尋找所有的符合條件的文件或者腳本,那么我們想要在這個總目錄下執行其中某個具體的腳本文件怎么辦?
[pytest] testpaths = ./scripts/ python_files = test_case_01.py
這么寫就是執行
scripts
目錄下面的test_case_01.py
這個文件。 - 因為我們用例可能分布在不同的目錄或文件中,那么這個
-
python_classes
則是說明腳本內的所有用例類名必須是以Test
開頭,當然,你也可以自定義為以Test_
開頭,而類中的用例方法則當然是以test_
開頭。 -
python_functions
則是說腳本內的所有用例函數以test_
開頭才能識別。
OK,來個示例。
首先,(詳細目錄參考開頭的目錄結構)在scripts/test_case_01.py
中:

import pytest def test_case01(): print('執行用例01.......') assert 1 # 斷言成功 def test_case02(): print('執行用例02.......') assert 1 # 斷言成功 class TestCaseClass(object): def test_case_03(self): assert 0 # 斷言失敗
在scripts/test_case_dir1/test_case02.py
中:

import pytest def test_case_04(): assert 1 # 斷言成功 def test_case_05(): assert 0 # 斷言失敗
那么,在不同的目錄或者文件中,共有5個用例將被執行,而結果則是兩個失敗三個成功。來執行驗證一下,因為有了配置文件,我們在終端中(前提是在項目的根目錄),直接輸入pytest
即可。

M:\py_tests>pytest ======================================================= test session starts ======================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests, inifile: pytest.ini, testpaths: ./scripts collected 5 items scripts\test_case_01.py 執行用例01....... .執行用例02....... .F scripts\test_case_dir1\test_case_02.py .F ============================================================= FAILURES ============================================================= ____________________________________________________ TestCaseClass.test_case_03 ____________________________________________________ self = <test_case_01.TestCaseClass object at 0x03CAF4D0> def test_case_03(self): > assert 0 E assert 0 scripts\test_case_01.py:22: AssertionError ___________________________________________________________ test_case_05 ___________________________________________________________ def test_case_05(): > assert 0 E assert 0 scripts\test_case_dir1\test_case_02.py:14: AssertionError =================================================== 2 failed, 3 passed in 0.14s ====================================================
由執行結果可以發現,2 failed, 3 passed
,跟我們的預期一致。
后續執行相關配置都來自配置文件,如果更改,會有相應說明,終端都是直接使用pytest
執行。
進階
跳過用例
我們知道在unittest中,跳過用例可以用skip
,那么這同樣是適用於pytest。
來看怎么使用:

import pytest @pytest.mark.skip(condition='我就是要跳過這個用例啦') def test_case_01(): assert 1 @pytest.mark.skipif(condition=1 < 2, reason='如果條件為true就跳過用例') def test_case_02(): assert 1
跳過用例,我們使用@pytest.mark.skipif(condition, reason)
:
- condition表示跳過用例的條件。
- reason表示跳過用例的原因。
然后將它裝飾在需要被跳過用例的的函數上面。
效果如下:

M:\py_tests>pytest scripts/test_allure_case.py::test_case_01 SKIPPED scripts/test_allure_case.py::test_case_02 SKIPPED =========================================================== 2 skipped in 0.14s ===========================================================
上例執行結果相對詳細,因為我們在配置文件中為addopts
增加了-v
,之前的示例結果中,沒有加!
標記預期失敗
所謂的預期失敗,就是希望用例執行失敗。這里用到了xfail
裝飾器:
xfail(condiition, reason, [raises=None, run=True, strict=False])
需要掌握的必傳參數的是:
- condition,預期失敗的條件,當條件為真的時候,預期失敗。
- reason,失敗的原因。
那么關於預期失敗的幾種情況需要了解一下:
- 預期失敗,執行成功。
- 預期失敗,執行失敗。
- 預期成功,執行成功。
- 預期成功,執行失敗。
來看示例:

import pytest class TestCase(object): @pytest.mark.xfail(1 < 2, reason='預期失敗, 執行失敗') def test_case_01(self): """ 預期失敗, 執行也是失敗的 """ print('預期失敗, 執行失敗') assert 0 @pytest.mark.xfail(1 < 2, reason='預期失敗, 執行成功') def test_case_02(self): """ 預期失敗, 但實際執行結果卻成功了 """ print('預期失敗, 執行成功') assert 1 @pytest.mark.xfail(1 > 2, reason='預期成功, 執行成功') def test_case_03(self): """ 預期成功, 實際執行結果成功 """ print('預期成功, 執行成功') assert 0 @pytest.mark.xfail(1 > 2, reason='預期成功, 執行失敗') def test_case_04(self): """ 預期成功, 但實際執行結果卻失敗了 """ print('預期成功, 執行失敗') assert 0 def test_case_05(self): """ 普通的測試用例 """ print('執行成功的普通用例') assert 1 def test_case_06(self): """ 普通的測試用例 """ print('執行失敗的普通用例') assert 0
結果如下:

M:\py_tests>pytest ===================================================== test session starts ====================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- c:\python36\python.exe cachedir: .pytest_cache metadata: {'Python': '3.6.2', 'Platform': 'Windows-10-10.0.14393-SP0', 'Packages': {'pytest': '5.2.2', 'py': '1.8.0', 'pluggy': ' 0.13.0'}, 'Plugins': {'allure-pytest': '2.8.6', 'cov': '2.8.1', 'forked': '1.1.3', 'html': '2.0.0', 'metadata': '1.8.0', 'orderin g': '0.6', 'rerunfailures': '7.0', 'xdist': '1.30.0'}, 'JAVA_HOME': 'C:\\Program Files\\Java\\jdk1.8.0_201'} rootdir: M:\py_tests, inifile: pytest.ini, testpaths: ./scripts/ plugins: allure-pytest-2.8.6, cov-2.8.1, forked-1.1.3, html-2.0.0, metadata-1.8.0, ordering-0.6, rerunfailures-7.0, xdist-1.30.0 collected 6 items scripts/test_case_01.py::TestCase::test_case_01 預期失敗, 執行失敗 XFAIL scripts/test_case_01.py::TestCase::test_case_02 預期失敗, 執行成功 XPASS scripts/test_case_01.py::TestCase::test_case_03 預期成功, 執行成功 FAILED scripts/test_case_01.py::TestCase::test_case_04 預期成功, 執行失敗 FAILED scripts/test_case_01.py::TestCase::test_case_05 執行成功的普通用例 PASSED scripts/test_case_01.py::TestCase::test_case_06 執行失敗的普通用例 FAILED =========================================================== FAILURES =========================================================== ____________________________________________________ TestCase.test_case_03 _____________________________________________________ self = <test_case_01.TestCase object at 0x03D2A470> @pytest.mark.xfail(1 > 2, reason='預期成功, 執行成功') def test_case_03(self): """ 預期成功, 實際執行結果成功 """ print('預期成功, 執行成功') > assert 0 E assert 0 scripts\test_case_01.py:27: AssertionError ____________________________________________________ TestCase.test_case_04 _____________________________________________________ self = <test_case_01.TestCase object at 0x03D2AF70> @pytest.mark.xfail(1 > 2, reason='預期成功, 執行失敗') def test_case_04(self): """ 預期成功, 但實際執行結果卻失敗了 """ print('預期成功, 執行失敗') > assert 0 E assert 0 scripts\test_case_01.py:33: AssertionError ____________________________________________________ TestCase.test_case_06 _____________________________________________________ self = <test_case_01.TestCase object at 0x03DDBEB0> def test_case_06(self): """ 普通的測試用例 """ print('執行失敗的普通用例') > assert 0 E assert 0 ====================================== 3 failed, 1 passed, 1 xfailed, 1 xpassed in 0.24s =======================================
由結果來看,3個失敗(failed),1個通過(passed),1個預期失敗並且執行失敗(xfailed),一個預期失敗但執行成功(xpassed)。
其中,對於之前說的4中情況來說:
- 預期失敗,執行成功,它的狀態是xpassed。
- 預期失敗,執行失敗,它的狀態是xfailed。
- 預期成功,執行成功,它的狀態是failed。
- 預期成功,執行失敗,它的狀態是failed。
可以看到,預期成功的那個兩個用例(用例3、4)無論是執行失敗還是成功,都跟普通的用例一樣(用例5、6)。所以,這里我們暫時可以忽略這兩種情況。只需要記住用例1、2這兩種情況即可,也就是預期失敗的兩種情況。
而在預期失敗的兩種情況中,我們不希望出現預期失敗,結果卻執行成功了的情況出現,因為跟我們想的不一樣嘛,我預期這條用例失敗,那這條用例就應該執行失敗才對,你雖然執行成功了,但跟我想的不一樣,你照樣是失敗的!
所以,我們需要將預期失敗,結果卻執行成功了的用例標記為執行失敗,可以在pytest.ini
文件中,加入:
[pytest]
xfail_strict=true
這樣就就把上述的情況標記為執行失敗了。
參數化
pytest身為強大的測試單元測試框架,那么同樣支持DDT數據驅動測試的概念。也就是當對一個測試函數進行測試時,通常會給函數傳遞多組參數。比如測試賬號登陸,我們需要模擬各種千奇百怪的賬號密碼。
當然,我們可以把這些參數寫在測試函數內部進行遍歷。不過雖然參數眾多,但仍然是一個測試,當某組參數導致斷言失敗,測試也就終止了。
通過異常捕獲,我們可以保證程所有參數完整執行,但要分析測試結果就需要做不少額外的工作。
在 pytest 中,我們有更好的解決方法,就是參數化測試,即每組參數都獨立執行一次測試。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)
。
- argnames表示參數名。
- argvalues表示列表形式的參數值。
使用就是以裝飾器的形式使用。
只有一個參數的測試用例
import pytest mobile_list = ['10010', '10086'] @pytest.mark.parametrize('mobile', mobile_list) def test_register(mobile): """ 通過手機號注冊 """ print('注冊手機號是: {}'.format(mobile))
來看(重要部分)結果:
M:\py_tests>pytest scripts/test_case_01.py::test_register[10010] 注冊手機號是: 10010 PASSED scripts/test_case_01.py::test_register[10086] 注冊手機號是: 10086 PASSED ====================================================== 2 passed in 0.11s ======================================================
可以看到,列表內的每個手機號,都是一條測試用例。
多個參數的測試用例
import pytest mobile_list = ['10010', '10086'] code_list = ['x2zx', 'we2a'] @pytest.mark.parametrize('mobile', mobile_list) @pytest.mark.parametrize('code', code_list) def test_register(mobile, code): """ 通過手機號注冊 """ print('注冊手機號是: {} 驗證碼是: {}'.format(mobile, code))
(重要部分)結果:
M:\py_tests>pytest scripts/test_case_01.py::test_register[x2zx-10010] 注冊手機號是: 10010 驗證碼是: x2zx PASSED scripts/test_case_01.py::test_register[x2zx-10086] 注冊手機號是: 10086 驗證碼是: x2zx PASSED scripts/test_case_01.py::test_register[we2a-10010] 注冊手機號是: 10010 驗證碼是: we2a PASSED scripts/test_case_01.py::test_register[we2a-10086] 注冊手機號是: 10086 驗證碼是: we2a PASSED ====================================================== 4 passed in 0.17s =======================================================
可以看到,每一個手機號與每一個驗證碼都組合一起執行了,這樣就執行了4次。那么如果有很多個組合的話,用例數將會更多。我們希望手機號與驗證碼一一對應組合,也就是只執行兩次,怎么搞呢?
import pytest mobile_list = ['10010', '10086'] code_list = ['x2zx', 'we2a'] @pytest.mark.parametrize('mobile,code', zip(mobile_list, code_list)) def test_register(mobile, code): """ 通過手機號注冊 """ print('注冊手機號是: {} 驗證碼是: {}'.format(mobile, code))
在多參數情況下,多個參數名是以,
分割的字符串。參數值是列表嵌套的形式組成的。
M:\py_tests>pytest scripts/test_case_01.py::test_register[10010-x2zx] 注冊手機號是: 10010 驗證碼是: x2zx PASSED scripts/test_case_01.py::test_register[10086-we2a] 注冊手機號是: 10086 驗證碼是: we2a PASSED ====================================================== 2 passed in 0.44s ======================================================
固件
什么是固件
固件(Fixture)是一些函數,pytest 會在執行測試函數之前(或之后)加載運行它們,也稱測試夾具。
我們可以利用固件做任何事情,其中最常見的可能就是數據庫的初始連接和最后關閉操作。
Pytest 使用 pytest.fixture()
定義固件,下面是最簡單的固件,訪問主頁前必須先登錄:

import pytest @pytest.fixture() def login(): print('登錄....') def test_index(login): print('主頁....')
結果:

M:\py_tests>pytest scripts/test_case_01.py::test_index 登錄.... 主頁.... PASSED ====================================================== 1 passed in 0.13s =======================================================
作用域
在之前的示例中,你可能會覺得,這跟之前的setup和teardown的功能也類似呀,但是,fixture相對於setup和teardown來說更靈活。pytest通過scope
參數來控制固件的使用范圍,也就是作用域。
在定義固件時,通過 scope
參數聲明作用域,可選項有:
function
: 函數級,每個測試函數都會執行一次固件;class
: 類級別,每個測試類執行一次,所有方法都可以使用;module
: 模塊級,每個模塊執行一次,模塊內函數和方法都可使用;session
: 會話級,一次測試只執行一次,所有被找到的函數和方法都可用。
默認的作用域為
function
。
比如之前的login固件,可以指定它的作用域:

import pytest @pytest.fixture(scope='function') def login(): print('登錄....') def test_index(login): print('主頁....')
預處理和后處理
很多時候需要在測試前進行預處理(如新建數據庫連接),並在測試完成進行清理(關閉數據庫連接)。
當有大量重復的這類操作,最佳實踐是使用固件來自動化所有預處理和后處理。
Pytest 使用 yield
關鍵詞將固件分為兩部分,yield
之前的代碼屬於預處理,會在測試前執行;yield
之后的代碼屬於后處理,將在測試完成后執行。
以下測試模擬數據庫查詢,使用固件來模擬數據庫的連接關閉:

import pytest @pytest.fixture() def db(): print('Connection successful') yield print('Connection closed') def search_user(user_id): d = { '001': 'xiaoming', '002': 'xiaohua' } return d[user_id] def test_case_01(db): assert search_user('001') == 'xiaoming' def test_case_02(db): assert search_user('002') == 'xiaohua' 結果: M:\py_tests>pytest scripts/test_case_01.py::test_case_01 Connection successful PASSEDConnection closed scripts/test_case_01.py::test_case_02 Connection successful PASSEDConnection closed ====================================================== 2 passed in 0.15s =======================================================
可以看到在兩個測試用例執行前后都有預處理和后處理。
常用插件
pytest中還有非常多的插件供我們使用,我們來介紹幾個常用的。
先來看一個重要的,那就是生成測試用例報告。
pytest測試報告插件
想要生成測試報告,首先要有下載,才能使用。
下載
pip install pytest-html
如果下載失敗,可以使用PyCharm下載,怎么用PyCharm下載這里無需多言了吧。
使用
在配置文件中,添加參數:
[pytest]
addopts = -s --html=report/report.html
完事之后,讓我們繼續終端中使用pytest
重新跑測試用例,用例結果就不展示了,跟上面的結果一樣,我們關注項目目錄下的report/report.html
文件,我們用瀏覽器打開它,你會發現:
效果很不錯吧!
沒完,看我大招 ↓
allure
Allure框架是一個靈活的輕量級多語言測試報告工具,它不僅以web的方式展示了簡介的測試結果,而且允許參與開發過程的每個人從日常執行的測試中最大限度的提取有用信息。
從開發人員(dev,developer)和質量保證人員(QA,Quality Assurance)的角度來看,Allure報告簡化了常見缺陷的統計:失敗的測試可以分為bug和被中斷的測試,還可以配置日志、步驟、fixture、附件、計時、執行歷史以及與TMS和BUG管理系統集成,所以,通過以上配置,所有負責的開發人員和測試人員可以盡可能的掌握測試信息。
從管理者的角度來看,Allure提供了一個清晰的“大圖”,其中包括已覆蓋的特性、缺陷聚集的位置、執行時間軸的外觀以及許多其他方便的事情。allure的模塊化和可擴展性保證了我們總是能夠對某些東西進行微調。
少扯點,來看看怎么使用。
Python的pytest中allure下載
pip install allure-pytest
但由於這個allure-pytest
插件生成的測試報告不是html
類型的,我們還需要使用allure工具再“加工”一下。所以說,我們還需要下載這個allure工具。
allure工具下載
在現在allure工具之前,它依賴Java環境,我們還需要先配置Java環境。
PS:Java JDK安裝包也在下面的百度雲鏈接中。
注意,如果你的電腦已經有了Java環境,就無需重新配置了。
配置完了Java環境,我們再來下載allure工具,我這里直接給出了百度雲盤鏈接,你也可以去其他鏈接中自行下載:
https://github.com/allure-framework/allure2
優先選擇:https://bintray.com/qameta/maven/allure2
百度雲盤鏈接:鏈接:https://pan.baidu.com/s/1Xj1A_xsRscOZHskTR4xjAg 提取碼:6b33
下載並解壓好了allure工具包之后,還需要將allure包內的bin
目錄添加到系統的環境變量中。
完事后打開你的終端測試:
C:\Users\Anthony\Desktop>allure --version
2.10.0
返回了版本號說明安裝成功。
使用
一般使用allure要經歷幾個步驟:
- 配置
pytest.ini
文件。 - 編寫用例並執行。
- 使用allure工具生成html報告。
來看配置pytest.ini
:
[pytest] addopts = -v -s --html=report/report.html --alluredir ./report/result testpaths = ./scripts/ python_files = test_allure_case.py python_classes = Test* python_functions = test_* # xfail_strict=true
就是--alluredir ./report/result
參數。
在終端中輸入pytest
正常執行測試用例即可:
import pytest def test_case_01(): assert 1 def test_case_02(): assert 0 def test_case_03(): assert 1
執行完畢后,在項目的根目下,會自動生成一個report
目錄,這個目錄下有:
- report.html是我們的之前的
pytest-html
插件生成的HTML報告,跟allure無關。 - result和assets目錄是allure插件生成的測試報告文件,但此時該目錄內還沒有什么HTML報告,只有一些相關數據。
接下來需要使用allure工具來生成HTML報告。
此時我們在終端(如果是windows平台,就是cmd),路徑是項目的根目錄,執行下面的命令。
PS:我在pycharm中的terminal輸入allure提示'allure' 不是內部或外部命令,也不是可運行的程序或批處理文件。但windows的終端沒有問題。
M:\py_tests>allure generate report/result -o report/allure_html --clean
Report successfully generated to report\allure_html
命令的意思是,根據report\result
目錄中的數據(這些數據是運行pytest后產生的)。在report
目錄下新建一個allure_html
目錄,而這個目錄內有index.html
才是最終的allure版本的HTML報告;如果你是重復執行的話,使用--clean
清除之前的報告。
結果很漂亮:
當然,故事還是沒有完!在使用allure生成報告的時候,在編寫用例階段,還可以有一些參數可以使用:
- title,自定義用例標題,標題默認是用例名。
- description,測試用例的詳細說明。
- feature和story被稱為行為驅動標記,因為使用這個兩個標記,通過報告可以更加清楚的掌握每個測試用例的功能和每個測試用例的測試場景。或者你可以理解為feature是模塊,而story是該模塊下的子模塊。
- allure中對severity級別的定義:
- Blocker級別:中斷缺陷(客戶端程序無響應,無法執行下一步操作)
- Critical級別:臨界缺陷( 功能點缺失)
- Normal級別:普通缺陷(數值計算錯誤)
- Minor級別:次要缺陷(界面錯誤與UI需求不符)
- Trivial級別:輕微缺陷(必輸項無提示,或者提示不規范)
- dynamic,動態設置相關參數。
allure.title與allure.description

import pytest import allure @allure.title('測試用例標題1') @allure.description('這是測試用例用例1的描述信息') def test_case_01(): assert 1 def test_case_02(): assert 0 def test_case_03(): assert 1
feature和story

import pytest import allure @allure.feature('登錄模塊') class TestCaseLogin(object): @allure.story('登錄模塊下的子模塊: test1') def test_case_01(self): assert 1 @allure.story('登錄模塊下的子模塊: test1') def test_case_02(self): assert 1 @allure.story('登錄模塊下的子模塊: test2') def test_case_03(self): assert 1 @allure.story('登錄模塊下的子模塊: test3') def test_case_04(self): assert 1 @allure.feature('注冊模塊') class TestCaseRegister(object): @allure.story('注冊模塊下的子模塊: test1') def test_case_01(self): assert 1 @allure.story('注冊模塊下的子模塊: test1') def test_case_02(self): assert 1 @allure.story('注冊模塊下的子模塊: test1') def test_case_03(self): assert 1 @allure.story('注冊模塊下的子模塊: test2') def test_case_04(self): assert 1
由上圖可以看到,不同的用例被分為不同的功能中。
allure.severity
allure.severity
用來標識測試用例或者測試類的級別,分為blocker,critical,normal,minor,trivial5個級別。

import pytest import allure @allure.feature('登錄模塊') class TestCaseLogin(object): @allure.severity(allure.severity_level.BLOCKER) def test_case_01(self): assert 1 @allure.severity(allure.severity_level.CRITICAL) def test_case_02(self): assert 1 @allure.severity(allure.severity_level.MINOR) def test_case_03(self): assert 1 @allure.severity(allure.severity_level.TRIVIAL) def test_case_04(self): assert 1 def test_case_05(self): assert 1
severity的默認級別是normal,所以上面的用例5可以不添加裝飾器了。
allure.dynamic

import pytest import allure @allure.feature('登錄模塊') class TestCaseLogin(object): @allure.severity(allure.severity_level.BLOCKER) def test_case_01(self): assert 1 @allure.severity(allure.severity_level.CRITICAL) def test_case_02(self): assert 1 @allure.severity(allure.severity_level.MINOR) def test_case_03(self): assert 1 @allure.severity(allure.severity_level.TRIVIAL) def test_case_04(self): assert 1 @pytest.mark.parametrize('name', ['動態名稱1', '動態名稱2']) def test_case_05(self, name): allure.dynamic.title(name)
控制用例執行順序
在之前,用例的執行順序是從上到下依次執行:

import pytest class TestCaseClass(object): def test_case_03(self): print('執行用例03.......') assert 1 def test_case01(): print('執行用例01.......') assert 1 # 斷成功 def test_case02(): print('執行用例02.......') assert 1 # 斷言成功
正如上例的執行順序是3 1 2
。
現在,來看看我們如何手動控制多個用例的執行順序,這里也依賴一個插件。
下載
pip install pytest-ordering
使用

import pytest class TestCaseClass(object): @pytest.mark.run(order=3) def test_case_03(self): print('執行用例03.......') assert 1 @pytest.mark.run(order=2) def test_case01(): print('執行用例01.......') assert 1 # 斷言成功 @pytest.mark.run(order=1) def test_case02(): print('執行用例02.......') assert 1 # 斷言成功
手動控制用例執行順序的方法是在給各用例添加一個裝飾器:
@pytest.mark.run(order=x) # x 是一個整數
那么, 現在的執行順序是2 1 3
,按照order指定的排序執行的。
如果有人較勁傳個0或者負數啥的,那么它們的排序關系應該是這樣的:
0 > 正數 > 沒有參與的用例 > 負數 # 正數和負數就是按照大小關系排列的
失敗重試
失敗重試意思是指定某個用例執行失敗可以重新運行。
下載
pip install pytest-rerunfailures
使用
需要在pytest.ini
文件中, 配置:
[pytest] addopts = -s --html=report/report.html --reruns=3 ;addopts = -s --alluredir ./report/result testpaths = ./scripts/ python_files = test_case_01.py python_classes = Test* python_functions = test_*
給addopts
字段新增(其他原有保持不變)--reruns=3
字段,這樣如果有用例執行失敗,則再次執行,嘗試3次。
來看示例:

import pytest def test_case01(): print('執行用例01.......') assert 1 # 斷言成功 def test_case02(): print('執行用例02.......') assert 0 # 斷言失敗,需要重新執行 class TestCaseClass(object): def test_case_03(self): print('執行用例03.......') assert 1 結果: M:\py_tests>pytest ======================================================= test session starts ======================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests, inifile: pytest.ini, testpaths: ./scripts/ plugins: allure-pytest-2.8.6, html-2.0.0, metadata-1.8.0, ordering-0.6, rerunfailures-7.0 collected 3 items scripts\test_case_01.py 執行用例01....... .執行用例02....... R執行用例02....... R執行用例02....... R執行用例02....... F執行用例03....... . ============================================================= FAILURES ============================================================= ___________________________________________________________ test_case02 ____________________________________________________________ def test_case02(): print('執行用例02.......') > assert 0 # 斷言失敗,需要重新執行 E assert 0 scripts\test_case_01.py:19: AssertionError ------------------------------------ generated html file: file://M:\py_tests\report\report.html ------------------------------------ =============================================== 1 failed, 2 passed, 3 rerun in 0.20s ===============================================
我們也可以從用例報告中看出重試的結果:
上面演示了用例失敗了,然后重新執行多少次都沒有成功,這是一種情況。
接下來,來看另一種情況,那就是用例執行失敗,重新執行次數內通過了,那么剩余的重新執行的次數將不再執行。

import random import pytest def test_case01(): print('執行用例01.......') assert 1 # 斷言成功 def test_case02(): print('執行用例02.......') status = random.randint(0, 2) if status: assert 1 # 斷言成功,無需再重復執行了 else: assert 0 # 斷言失敗,需要重新執行 class TestCaseClass(object): def test_case_03(self): print('執行用例03.......') assert 1
通過random
模塊幫助我們演示出在某次執行中出現失敗的情況,而在重新執行的時候,會出現成功的情況,看結果:

M:\py_tests>pytest ======================================================= test session starts ======================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 rootdir: M:\py_tests, inifile: pytest.ini, testpaths: ./scripts/ plugins: allure-pytest-2.8.6, html-2.0.0, metadata-1.8.0, ordering-0.6, rerunfailures-7.0 collected 3 items scripts\test_case_01.py 執行用例01....... .執行用例02....... R執行用例02....... .執行用例03....... . ------------------------------------ generated html file: file://M:\py_tests\report\report.html ------------------------------------ ==================================================== 3 passed, 1 rerun in 0.08s ====================================================
可以看到,用例02
重新執行了一次就成功了,剩余的兩次執行就終止了。
並發執行
一條一條用例的執行,肯定會很慢,來看如何並發的執行測試用例,當然這需要相應的插件。
下載
pip install pytest-xdist
使用
在配置文件中添加:
[pytest] addopts = -v -s --html=report/report.html -n=auto ;addopts = -s --alluredir ./report/result testpaths = ./scripts/ python_files = test_case_01.py python_classes = Test* python_functions = test_*
就是這個-n=auto
:
-n=auto
,自動偵測系統里的CPU數目。-n=numprocesses
,也就是自己指定運行測試用例的進程數。
並發的配置可以寫在配置文件中,然后其他正常的執行用例腳本即可。另外一種就是在終端中指定,先來看示例:

import pytest def test_case01(): print('執行用例01.......') assert 1 # 斷言成功 @pytest.mark.skipif(condition= 2 > 1, reason='跳過用例') def test_case02(): print('執行用例02.......') assert 0 # 斷言失敗 class TestCaseClass(object): def test_case_03(self): print('執行用例03.......') assert 1 def test_case_04(self): print('執行用例04.......') assert 1 結果: M:\py_tests>pytest .\scripts\test_case_01.py -s -n auto ======================================================= test session starts ======================================================== platform win32 -- Python 3.6.2, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- c:\python36\python.exe cachedir: .pytest_cache metadata: {'Python': '3.6.2', 'Platform': 'Windows-10-10.0.14393-SP0', 'Packages': {'pytest': '5.2.2', 'py': '1.8.0', 'pluggy': '0.13 .0'}, 'Plugins': {'allure-pytest': '2.8.6', 'forked': '1.1.3', 'html': '2.0.0', 'metadata': '1.8.0', 'ordering': '0.6', 'rerunfailure s': '7.0', 'xdist': '1.30.0'}, 'JAVA_HOME': 'C:\\Program Files\\Java\\jdk1.8.0_201'} rootdir: M:\py_tests, inifile: pytest.ini plugins: allure-pytest-2.8.6, forked-1.1.3, html-2.0.0, metadata-1.8.0, ordering-0.6, rerunfailures-7.0, xdist-1.30.0 [gw0] win32 Python 3.6.2 cwd: M:\py_tests [gw1] win32 Python 3.6.2 cwd: M:\py_tests [gw2] win32 Python 3.6.2 cwd: M:\py_tests [gw3] win32 Python 3.6.2 cwd: M:\py_tests [gw0] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw1] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw2] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw3] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] gw0 [4] / gw1 [4] / gw2 [4] / gw3 [4] scheduling tests via LoadScheduling scripts/test_case_01.py::test_case02 scripts/test_case_01.py::TestCaseClass::test_case_04 scripts/test_case_01.py::TestCaseClass::test_case_03 scripts/test_case_01.py::test_case01 [gw3] PASSED scripts/test_case_01.py::TestCaseClass::test_case_04 [gw0] PASSED scripts/test_case_01.py::test_case01 [gw2] PASSED scripts/test_case_01.py::TestCaseClass::test_case_03 [gw1] SKIPPED scripts/test_case_01.py::test_case02 ------------------------------------ generated html file: file://M:\py_tests\report\report.html ------------------------------------ =================================================== 3 passed, 1 skipped in 2.23s ===================================================
pytest-sugar
pytest-sugar 改變了 pytest
的默認外觀,添加了一個進度條,並立即顯示失敗的測試。它不需要配置,只需 下載插件即可,用 pytest
運行測試,來享受更漂亮、更有用的輸出。
下載
pip install pytest-sugar
其他照舊執行用例即可。
pytest-cov
pytest-cov 在 pytest
中增加了覆蓋率支持,來顯示哪些代碼行已經測試過,哪些還沒有。它還將包括項目的測試覆蓋率。
下載
pip install pytest-cov
使用
在配置文件中:
[pytest] addopts = -v -s --html=report/report.html -n=auto --cov=./scripts ;addopts = -s --alluredir ./report/result testpaths = ./scripts/ python_files = test_case_01.py python_classes = Test* python_functions = test_*
也就是配置--cov=./scripts
,這樣,它就會統計所有scripts
目錄下所有符合規則的腳本的測試覆蓋率。
執行的話,就照常執行就行。
結果:

M:\py_tests>pytest Test session starts (platform: win32, Python 3.6.2, pytest 5.2.2, p ytest-sugar 0.9.2) cachedir: .pytest_cache metadata: {'Python': '3.6.2', 'Platform': 'Windows-10-10.0.14393-SP 0', 'Packages': {'pytest': '5.2.2', 'py': '1.8.0', 'pluggy': '0.13. 0'}, 'Plugins': {'allure-pytest': '2.8.6', 'cov': '2.8.1', 'forked' : '1.1.3', 'html': '2.0.0', 'metadata': '1.8.0', 'ordering': '0.6', 'rerunfailures': '7.0', 'sugar': '0.9.2', 'xdist': '1.30.0'}, 'JAV A_HOME': 'C:\\Program Files\\Java\\jdk1.8.0_201'} rootdir: M:\py_tests, inifile: pytest.ini, testpaths: ./scripts/ plugins: allure-pytest-2.8.6, cov-2.8.1, forked-1.1.3, html-2.0.0, metadata-1.8.0, ordering-0.6, rerunfailures-7.0, sugar-0.9.2, xdist -1.30.0 [gw0] win32 Python 3.6.2 cwd: M:\py_tests [gw1] win32 Python 3.6.2 cwd: M:\py_tests [gw2] win32 Python 3.6.2 cwd: M:\py_tests [gw3] win32 Python 3.6.2 cwd: M:\py_tests [gw0] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw1] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw2] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] [gw3] Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] gw0 [4] / gw1 [4] / gw2 [4] / gw3 [4] scheduling tests via LoadScheduling scripts\test_case_01.py::test_case02 s 50% █████ pts\test_case_01.py::test_case01 ✓ 25% ██▌ ] PASSED scripts/test_case_01.py scripts\test_case_01.py::TestCaseClass.test_case_03 ✓ 75% ████ ███▌ scripts\test_case_01.py::TestCaseClass.test_case_04 ✓ 100% ████ ██████ scripts/test_case_01.py Coverage.py warning: No data was collected. (no-data-collected) ----------------------------------------- generated html file: file://M:\py_tests\report\report.html ----------------------------------------- ----------- coverage: platform win32, python 3.6.2-final-0 ----------- Name Stmts Miss Cover ------------------------------------------------- scripts\demo1.py 4 4 0% scripts\test_allure_case.py 7 7 0% scripts\test_case_01.py 15 2 87% ------------------------------------------------- TOTAL 26 13 50% Results (2.58s): 3 passed 1 skipped
更多插件參考:https://zhuanlan.zhihu.com/p/50317866
常見問題
有的時候,在pytest.ini
中配置了pytest-html
和allure
插件之后,執行后報錯:
出現了這個報錯,檢查你配置的解釋器中是否存在pytest-html
和allure-pytest
這兩個模塊。如果是使用的pycharm ide,那么你除了檢查settings中的解釋器配置之外,還需要保證運行腳本的編輯器配置是否跟settings中配置一致。
see also:8 個很棒的 pytest 插件 | Pytest【pytest-xdist】並行運行測試用例 | Pytest 使用手冊 | pytest參數化 |Allure-pytest功能特性介紹