本文節(jié)選自霍格沃玆測試學院內部教材,進階學習。
在上一篇文章中分享了 pytest 的基本用法,本文進一步介紹 pytest 的其他實用特性和進階技巧。
pytest fixtures
pytest 中可以使用 @pytest.fixture 裝飾器來裝飾一個方法,被裝飾方法的方法名可以作為一個參數傳入到測試方法中。可以使用這種方式來完成測試之前的初始化,也可以返回數據給測試函數。
將 fixture 作為函數參數
通常使用 setup 和 teardown 來進行資源的初始化。如果有這樣一個場景,測試用例 1 需要依賴登錄功能,測試用例 2 不需要登錄功能,測試用例 3 需要登錄功能。這種場景 setup,teardown 無法實現,可以使用 pytest fixture 功能,在方法前面加個 @pytest.fixture 裝飾器,加了這個裝飾器的方法可以以參數的形式傳入到方法里面執(zhí)行。
例如在登錄的方法,加上 @pytest.fixture 這個裝飾器后,將這個用例方法名以參數的形式傳到方法里,這個方法就會先執(zhí)行這個登錄方法,再去執(zhí)行自身的用例步驟,如果沒有傳入這個登錄方法,就不執(zhí)行登錄操作,直接執(zhí)行已有的步驟。
創(chuàng)建一個文件名為“test_fixture.py”,代碼如下:
#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest@pytest.fixture()def login(): print("這是個登錄方法") return ('tom','123')@pytest.fixture()def operate(): print("登錄后的操作")def test_case1(login,operate): print(login) print("test_case1,需要登錄")def test_case2(): print("test_case2,不需要登錄 ")def test_case3(login): print(login) print("test_case3,需要登錄")
在上面的代碼中,測試用例 test_case1 和 test_case3 分別增加了 login 方法名作為參數,pytest 會發(fā)現并調用 @pytest.fixture 標記的 login 功能,運行測試結果如下:
plugins: html-2.0.1, rerunfailures-8.0, xdist-1.31.0, \ordering-0.6, forked-1.1.3, allure-pytest-2.8.11, metadata-1.8.0collecting ... collected 3 itemstest_fixture.py::test_case1 這是個登錄方法登錄后的操作PASSED [ 33%]('tom', '123')test_case1,需要登錄test_fixture.py::test_case2 PASSED \[ 66%]test_case2,不需要登錄 test_fixture.py::test_case3 這是個登錄方法PASSED [100%]('tom', '123')test_case3,需要登錄============================== 3 passed in 0.02s ===============================Process finished with exit code 0
從上面的結果可以看出,test_case1 和 test_case3 運行之前執(zhí)行了 login 方法,test_case2 沒有執(zhí)行這個方法。
指定范圍內共享
fixture 里面有一個參數 scope,通過 scope 可以控制 fixture 的作用范圍,根據作用范圍大小劃分:session> module> class> function,具體作用范圍如下:
- function 函數或者方法級別都會被調用
- class 類級別調用一次
- module 模塊級別調用一次
- session 是多個文件調用一次(可以跨.py文件調用,每個.py文件就是module)
例如整個模塊有多條測試用例,需要在全部用例執(zhí)行之前打開瀏覽器,全部執(zhí)行完之后去關閉瀏覽器,打開和關閉操作只執(zhí)行一次,如果每次都重新執(zhí)行打開操作,會非常占用系統(tǒng)資源。這種場景除了setup_module,teardown_module 可以實現,還可以通過設置模塊級別的 fixture 裝飾器(@pytest.fixture(scope="module"))來實現。
scope='module'
fixture 參數 scope='module',module 作用是整個模塊都會生效。
創(chuàng)建文件名為 test_fixture_scope.py,代碼如下:
#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest# 作用域:module是在模塊之前執(zhí)行, 模塊之后執(zhí)行@pytest.fixture(scope="module")def open(): print("打開瀏覽器") yield print("執(zhí)行teardown !") print("最后關閉瀏覽器")@pytest.mark.usefixtures("open")def test_search1(): print("test_search1") raise NameError passdef test_search2(open): print("test_search2") passdef test_search3(open): print("test_search3") pass
代碼解析:
@pytest.fixture() 如果不寫參數,參數默認 scope='function'。當 scope='module' 時,在當前 .py 腳本里面所有的用例開始前只執(zhí)行一次。scope 巧妙與 yield 組合使用,相當于 setup 和 teardown 方法。還可以使用 @pytest.mark.usefixtures 裝飾器,傳入前置函數名作為參數。
運行結果如下:
plugins: html-2.0.1, rerunfailures-8.0, \xdist-1.31.0, ordering-0.6, forked-1.1.3,\ allure-pytest-2.8.11, metadata-1.8.0collecting ... collected 3 itemstest_fixture_yield.py::test_search1 打開瀏覽器FAILED [ 33%]test_search1test_fixture_yield.py:13 (test_search1)open = None def test_search1(open): print("test_search1")> raise NameErrorE NameErrortest_fixture_yield.py:16: NameErrortest_fixture_yield.py::test_search2 PASSED \[ 66%]test_search2test_fixture_yield.py::test_search3 PASSED \[100%]test_search3執(zhí)行teardown !最后關閉瀏覽器...open = None def test_search1(open): print("test_search1")> raise NameErrorE NameErrortest_fixture_yield.py:16: NameError------ Captured stdout setup --------打開瀏覽器----- Captured stdout call -----test_search1===== 1 failed, 2 passed in 0.06s =====Process finished with exit code 0
從上面運行結果可以看出,scope="module" 與 yield 結合,相當于 setup_module 和 teardown_module 方法。整個模塊運行之前調用了 open()方法中 yield 前面的打印輸出“打開瀏覽器”,整個運行之后調用了 yield 后面的打印語句“執(zhí)行 teardown !”與“關閉瀏覽器”。yield 來喚醒 teardown 的執(zhí)行,如果用例出現異常,不影響 yield 后面的 teardown 執(zhí)行。可以使用 @pytest.mark.usefixtures 裝飾器來進行方法的傳入。
conftest.py 文件
fixture scope 為 session 級別是可以跨 .py 模塊調用的,也就是當我們有多個 .py 文件的用例時,如果多個用例只需調用一次 fixture,可以將 scope='session',并且寫到 conftest.py 文件里。寫到 conftest.py 文件可以全局調用這里面的方法。使用的時候不需要導入 conftest.py 這個文件。使用 conftest.py 的規(guī)則:
- conftest.py 這個文件名是固定的,不可以更改。
- conftest.py 與運行用例在同一個包下,并且該包中有 init.py 文件
- 使用的時候不需要導入 conftest.py,pytest 會自動識別到這個文件
- 放到項目的根目錄下可以全局調用,放到某個 package 下,就在這個 package 內有效。
案例
在運行整個項目下的所有的用例,只執(zhí)行一次打開瀏覽器。執(zhí)行完所有的用例之后再執(zhí)行關閉瀏覽器,可以在這個項目下創(chuàng)建一個 conftest.py 文件,將打開瀏覽器操作的方法放在這個文件下,并添加一個裝飾器 @pytest.fixture(scope="session"),就能夠實現整個項目所有測試用例的瀏覽器復用,案例目錄結構如下:
創(chuàng)建目錄 test_scope,并在目錄下創(chuàng)建三個文件 conftest.py,test_scope1.py 和 test_scope2.py。
conftest.py 文件定義了公共方法,pytest 會自動讀取 conftest.py 定義的方法,代碼如下:
#!/usr/bin/env python# -*- coding: utf-8 -*-import pytest@pytest.fixture(scope="session")def open(): print("打開瀏覽器") yield print("執(zhí)行teardown !") print("最后關閉瀏覽器")
創(chuàng)建 test_scope1.py 文件,代碼如下:
#!/usr/bin/env python# -*- coding: utf-8 -*-import pytestdef test_search1(open): print("test_search1") passdef test_search2(open): print("test_search2") passdef test_search3(open): print("test_search3") passif __name__ == '__main__': pytest.main()
創(chuàng)建文件“test_scope2.py”,代碼如下:
#!/usr/bin/env python# -*- coding: utf-8 -*-class TestFunc(): def test_case1(self): print("test_case1,需要登錄") def test_case2(self): print("test_case2,不需要登錄 ") def test_case3(self): print("test_case3,需要登錄")
打開 cmd,進入目錄 test_scope/,執(zhí)行如下命令:
pytest -v -s
或者
pytest -v -s test_scope1.py test_scope2.py
執(zhí)行結果如下:
省略...collected 6 items test_scope1.py::test_search1 打開瀏覽器test_search1PASSEDtest_scope1.py::test_search2 test_search2PASSEDtest_scope1.py::test_search3 test_search3PASSEDtest_scope2.py::TestFunc::test_case1 test_case1,需要登錄PASSEDtest_scope2.py::TestFunc::test_case2 test_case2,不需要登錄 PASSEDtest_scope2.py::TestFunc::test_case3 test_case3,需要登錄PASSED執(zhí)行teardown !最后關閉瀏覽器省略后面打印結果...
執(zhí)行過程中 pytest 會自動識別當前目錄的 conftest.py,不需要導入直接引用里面的方法配置。應用到整個目錄下的所有調用這里面的方法中執(zhí)行。conftest.py 與運行的用例要在同一個 pakage 下,并且這個包下有 init.py 文件
自動執(zhí)行 fixture
如果每條測試用例都需要添加 fixture 功能,則需要在每一要用例方法里面?zhèn)魅脒@個fixture的名字,這里就可以在裝飾器里面添加一個參數 autouse='true',它會自動應用到所有的測試方法中,只是這里沒有辦法返回值給測試用例。
使用方法,在方法前面加上裝飾器,如下:
@pytest.fixture(autouse="true")def myfixture(): print("this is my fixture")
@pytest.fixture 里設置 autouse 參數值為 true(默認 false),每個測試函數都會自動調用這個前置函數。
創(chuàng)建文件名為“test_autouse.py”,代碼如下:
# coding=utf-8import pytest@pytest.fixture(autouse="true")def myfixture(): print("this is my fixture")class TestAutoUse: def test_one(self): print("執(zhí)行test_one") assert 1 + 2 == 3 def test_two(self): print("執(zhí)行test_two") assert 1 == 1 def test_three(self): print("執(zhí)行test_three") assert 1 + 1 == 2
執(zhí)行上面這個測試文件,結果如下:
...test_a.py::TestAutoUse::test_one this is my fixture執(zhí)行test_onePASSEDtest_a.py::TestAutoUse::test_two this is my fixture執(zhí)行test_twoPASSEDtest_a.py::TestAutoUse::test_three this is my fixture執(zhí)行test_threePASSED...
從上面的運行結果可以看出,在方法 myfixture() 上面添加了裝飾器 @pytest.fixture(autouse="true"),測試用例無須傳入這個 fixture 的名字,它會自動在每條用例之前執(zhí)行這個 fixture。
fixture 傳遞參數
測試過程中需要大量的測試數據,如果每條測試數據都編寫一條測試用例,用例數量將是非常寵大的。一般我們在測試過程中會將測試用到的數據以參數的形式傳入到測試用例中,并為每條測試數據生成一個測試結果數據。
這時候可以使用 fixture 的參數化功能,在 fixture 方法加上裝飾器 @pytest.fixture(params=[1,2,3]),就會傳入三個數據 1、2、3,分別將這三個數據傳入到用例當中。這里可以傳入的數據是個列表。傳入的數據需要使用一個固定的參數名 request 來接收。
創(chuàng)建文件名為“test_params.py”,代碼如下:
import pytest@pytest.fixture(params=[1, 2, 3])def data(request): return request.paramdef test_not_2(data): print(f"測試數據:{data}") assert data < 5
運行結果如下:
...test_params.py::test_not_2[1]PASSED [ 33%]測試數據:1test_params.py::test_not_2[2] PASSED [ 66%]測試數據:2test_params.py::test_not_2[3] PASSED [100%]測試數據:3...
從運行結果可以看出,對于 params 里面的每個值,fixture 都會去調用執(zhí)行一次,使用 request.param 來接受用例參數化的數據,并且為每一個測試數據生成一個測試結果。在測試工作中使用這種參數化的方式,會減少大量的代碼量,并且便于閱讀與維護。
多線程并行與分布式執(zhí)行
假如項目中有測試用例 1000 條,一條測試用例需要執(zhí)行 1 分鐘,一個測試人員需要 1000 分鐘才能完成一輪回歸測試。通常我們會用人力成本換取時間成本,加幾個人一起執(zhí)行,時間就會縮短。如果 10 人一起執(zhí)行只需要 100 分鐘,這就是一種并行測試,分布式的場景。
pytest-xdist 是 pytest 分布式執(zhí)行插件,可以多個 CPU 或主機執(zhí)行,這款插件允許用戶將測試并發(fā)執(zhí)行(進程級并發(fā)),插件是動態(tài)決定測試用例執(zhí)行順序的,為了保證各個測試能在各個獨立線程里正確的執(zhí)行,應該保證測試用例的獨立性(這也符合測試用例設計的最佳實踐)。
安裝
pip install pytest-xdist
多個 CPU 并行執(zhí)行用例,需要在 pytest 后面添加 -n 參數,如果參數為 auto,會自動檢測系統(tǒng)的 CPU 數目。如果參數為數字,則指定運行測試的處理器進程數。
pytest -n auto pytest -n [num]
案例
某個項目有 200 條測試用例,每條測試用例之間沒有關聯(lián)關系,互不影響。這 200 條測試用例需要在 1 小時之內測試完成,可以加個-n參數,使用多 CPU 并行測試。運行方法:
pytest -n 4
進入到項目目錄下,執(zhí)行 pytest 可以將項目目錄下所有測試用例識別出來并且運行,加上 -n 參數,可以指定 4 個 CPU 并發(fā)執(zhí)行。大量的測試用例并發(fā)執(zhí)行提速非常明顯。
結合 pytest-html 生成測試報告
測試報告通常在項目中尤為重要,報告可以體現測試人員的工作量,開發(fā)人員可以從測試報告中了解缺陷的情況,因此測試報告在測試過程中的地位至關重要,測試報告為糾正軟件存在的質量問題提供依據,為軟件驗收和交付打下基礎。測試報告根據內容的側重點,可以分為 “版本測試報告” 和 “總結測試報告”。執(zhí)行完 pytest 測試用例,可以使用 pytest-HTML 插件生成 HTML 格式的測試報告。
安裝
pip install pytest-html
執(zhí)行方法
pytest --html=path/to/html/report.html
結合 pytest-xdist 使用
pytest -v -s -n 3 --html=report.html --self-contained-html
生成測試報告
如下圖:
生成的測試報告最終是 HTML 格式,報告內容包括標題、運行時間、環(huán)境、匯總結果以及用例的通過個數、跳過個數、失敗個數、錯誤個數,期望失敗個數、不期望通過個數、重新運行個數、以及錯誤的詳細展示信息。報告會生成在運行腳本的同一路徑,需要指定路徑添加--html=path/to/html/report.html 這個參數配置報告的路徑。如果不添加 --self-contained-html 這個參數,生成報告的 CSS 文件是獨立的,分享的時候容易千萬數據丟失。
pytest 框架 assert 斷言使用(附)
編寫代碼時,我們經常會做出一些假設,斷言就是用于在代碼中捕捉這些假設。斷言表示為一些布爾表達式,測試人員通常會加一些斷言來斷定中間過程的正確性。斷言支持顯示最常見的子表達式的值,包括調用,屬性,比較以及二元和一元運算符。Python使用 assert(斷言)用于判斷一個表達式,在表達式條件為 false 的時候觸發(fā)異常。
使用方法:
assert True #斷言為真assertnot False #斷言為假
案例如下:
assert "h" in "hello" #判斷h在hello中assert 5>6 #判斷5>6為真 assert not True #判斷xx不為真assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'} #判斷兩個字典相等
如果沒有斷言,沒有辦法判定用例中每一個測試步驟結果的正確性。在項目中適當的使用斷言,來對代碼的結構、屬性、功能、安全性等場景檢查與驗證。
點擊領取:自動化+側開+性能+簡歷+面試核心教程資料
http://qrcode.testing-studio.com/f?from=jianshu&url=https://ceshiren.com/t/topic/3595