一、fixture基本操作介紹
雖然pytest在unittest的兩組前置后置方法方法基礎上,提供了更全面的總共五組的前置后置方法,但這些方法都是針對各自對應的整個作用域全局生效的,
如果有以下場景:用例 1 需要先登錄,用例 2 不需要登錄,用例 3 需要先登錄。很顯然無法用 setup 和 teardown 來實現了
pytest框架的精髓fixture可以讓我們隨心所欲的定制測試用例的前置后置方法
fixture是pytest將測試前后進行預備、清理工作的代碼分離出核心測試邏輯的一種機制
1.基本形式和用法:
@pytest.fixture() 裝飾器用於聲明函數是一個fixture,該fixture的名字默認為函數名,也可以自己指定名稱(詳見參數name解釋)
如果測試用例的參數列表中包含fixture的名字,那么pytest會根據名字檢測到該fixture,並在測試函數運行之前執行該fixture
fixture可以完成測試任務,也可以返回數據給測試用例
import pytest @pytest.fixture(scope="function", params=None, autouse=False, ids=None, name=None) def func_01(): print("這就定義了一個簡單的fixture") return '結果' def test_01(func_01): print(type(func_01), func_01) # <class 'str'> 結果 if __name__ == '__main__': pytest.main()
2.檢測順序:
檢測順序是:當前測試類 > 模塊(.py文件)> 當前包中conftest.py > 父包中conftest.py > 根目錄中conftest.py
3.存放位置
- 可以放在測試用例自己的測試文件里
- 如果希望多個測試文件共享fixture,可以放在某個公共目錄下新建一個conftest.py 點擊查看,將fixture放在里面
4.fixture調用方式
注意事項: 如果不是測試用例,無論哪種調用方式都不會生效(參考func_006) 未傳入fixture,不會執行仍何fixture
1)參數傳參:
將fixture函數名當參數傳入用例(函數名無引號)
支持多個,支持fixture相互調用時給fixture傳參
返回值:fixture執行完畢將返回值賦值給用例參數名,無返回值默認為None
2)裝飾器傳參:
支持多個,不支持fixture相互調用時給fixture傳參
返回值:不能獲取
第一種:傳入名字,@pytest.mark.usefixtures("fixture1", "fixture2") (字符串格式,帶引號的)
第二種:多個可以使用@pytest.mark.usefixture()進行疊加,先執行的放底層,后執行的放上層

3)autouse=True,自動調用,詳見下一節: 其他參數使用
5.fixture實例化順序
- 高級別scope的fixture在低級別scope的fixture之前實例化(session > package > module > class > function)
- 具有相同scope的fixture遵循測試函數中聲明的順序
- 遵循fixture之間的依賴關系【在fixture_A里面依賴的fixture_B優先實例化,然后到fixture_A實例化】
- (autouse=True)自動使用的fixture將在顯式使用(傳參或裝飾器)的fixture之前實例化
上面的規則是基本前提,當遇到不同級別fixture相互調用的情況時,實例化順序會很復雜讓人頭疼:(使用需謹慎)

import pytest order = [] @pytest.fixture(scope="session") def s1(): order.append("s1") @pytest.fixture(scope="session") def s2(): order.append("s2") @pytest.fixture(scope="session") def s3(): order.append("s3") @pytest.fixture(scope="session") def s4(): order.append("s4") @pytest.fixture(scope="session") def s5(s7): order.append("s5") @pytest.fixture(scope="session") def s6(): order.append("s6") @pytest.fixture(scope="session") def s7(): order.append("s7") @pytest.fixture(scope="module") def m1(): order.append("m1") @pytest.fixture(scope="module") def m2(s5): order.append("m2") @pytest.fixture(scope="module") def m3(s4): order.append("m3") @pytest.fixture def f1(s2, f3): order.append("f1") @pytest.fixture def f2(m2, s3): order.append("f2") @pytest.fixture def f3(s6): order.append("f3") def test_order(f2, f1, m3, m1, s1): print(order) # ['s1', 's3', 's2', 's4', 's7', 's5', 's6', 'm3', 'm1', 'm2', 'f2', 'f3', 'f1'] if __name__ == '__main__': pytest.main()
可以先畫出繼承關系圖,這樣就會很明了,按着等級去找就對了:
在實例化基本規則大前提下,fixture可能存在不同等級之間的相互調用,這就存在依賴深度等級,如圖:
1.所有的session級必定最先執行,接下來是確定該級別fixture的先后順序:
1)先運行第一等級的session級別,運行s1
2)第二等級按照傳入(f2, f1, m3, m1, s1)傳入順序依次查找,得到 s3 > s2 > s4
3)第三等級按照傳入(f2, f1, m3, m1, s1)傳入順序依次查找,得到 (s5依賴於同scope級別s7)s7 > s5 > s6
2.接下來運行module級別
1)第一等級按照傳入順序 m3 > m1
2)第二等級按照傳入(f2, f1, m3, m1, s1)傳入順序依次查找,得到 m2
3.運行function級別
1)第一等級按照傳入順序 f2 > f3 > f1 (f1依賴於同scope級別f3)
走完以上流程得到最終結果: ['s1', 's3', 's2', 's4', 's7', 's5', 's6', 'm3', 'm1', 'm2', 'f2', 'f3', 'f1']
二、scope參數詳解(fixture的作用范圍)
fixture里面scope參數可以控制fixture的作用范圍:session > module > class > function(默認)
fixture可相互調用,但要注意:如果級別不同,低級別可以調用高級別,高級別不能調用低級別
- function:每一個調用了該fixture的函數或方法執行前都會執行一次,在測試用例執行之后運行銷毀代碼
- class:每一個類只調用一次,一個類中可以有多個用例調用,但是只在最早運行的用例之前調用一次(每一個類外函數用例也都會調用一次)
- module:每一個.py文件調用一次,該模塊內最先執行的調用了該fixture的用例執行前運行且只運行一次
- session:是多個文件調用一次,可以跨.py文件調用,每個.py文件就是module
1.scope=function 函數級(默認級別)
最基本的fixture
1.執行時機
每一個調用了該fixture的函數或方法執行前都會執行一次,在測試用例執行之后運行銷毀代碼
2.fixture可相互調用(參考test_002的login_and_logout) 1)fixture可以像測試用例的參數傳參一樣調用其他的fixture,並獲取其返回值 2)多個fixture的執行順序和測試用例調用的情況是一樣的
3)只支持參數傳入fixture,不支持裝飾器傳參
代碼:

import pytest @pytest.fixture def login(): print("打開瀏覽器") return 'chrome' @pytest.fixture() def logout(): print("關閉瀏覽器") @pytest.fixture() def login_and_logout(logout, login): # fixture的相互調用,支持調用多個,執行順序:loguot > login > login_and_logout print(f"先打開{login},又關閉了它") return f'{login} + {logout}' def test_005(login): # 第一種傳參:fixture的名字作為參數傳參============================================================================= print(f'005: login={login}') class TestLogin: def test_001(self, login): print(f"001: login={login}") def test_003(self, login, logout): # 支持傳多個,執行順序按照傳入順序:login > logout > test_003 print(f"003: login={login} logout={logout}") def test_002(self, login_and_logout): print(f"002: login_and_logout={login_and_logout}") def test_004(self): print("004:未傳入,不會執行仍何fixture") @pytest.mark.usefixtures("login", "logout") # 第二種傳參:裝飾器傳參,傳入fixture的str,這種不能獲取返回值============== def test_005(self): print(f"005: 這樣傳參無法獲取到fixture的返回值") @pytest.mark.usefixtures("login", "logout") def func_006(self): print("006:不是測試用例,加了裝飾器也不會執行fixture") if __name__ == '__main__': pytest.main()
2.scope=class 類級別
1.調用方式:
和function一樣
2.運行時機:
1)類外的獨立函數用例執行前會執行一次,參考test_006
2)類中有用例調用了fixture,則會在最先調用的那個用例執行前執行一次(參考test_002),該類下其他調用了fixture的用例不再執行(參考test_003)
3)未調用該fixture的類和函數,不會運行
2.fixture相互調用規則
1)類級可以調用類級,參考test_004
2)函數級可以調用類級,參考test_008
3)類級不可以調用函數級,參考test_007
代碼:

import pytest @pytest.fixture(scope='class') def login(): print("打開瀏覽器 -- 類級別fixture") return 'chrome' @pytest.fixture() def logout(): print("關閉瀏覽器 -- 函數級fixture") @pytest.fixture(scope='class') def class_use_class(login): print(f"class_use_class -- 類級可以調用類級") return f'{login}' @pytest.fixture() def function_use_class(login): print(f"function_use_class -- 函數級調用類級") @pytest.fixture(scope='class') def class_use_function(logout): print(f"錯誤示范,類級不可以調用函數級") def test_006(login): print(f'006: login={login}') class TestLogin: def test_001(self, logout): print(f"001: login={logout} 調用普通函數級") def test_002(self, login): print(f"002:logout={login} 調用類級,該類中login只會在這里運行一次") def test_003(self, login, logout): print(f"003: login={login} logout={logout} 調用類級和函數級,該類中類級login不會再運行") @pytest.mark.usefixtures("class_use_class") def test_004(self, class_use_class): print(f"004: class_use_class 調用類級-->類再調用類級login,運行過了不會再運行") # def test_004(self, class_use_class): # print(f"004: class_use_class={class_use_class} 調用類級-->類再調用類級login,運行過了不會再運行") def test_007(self, class_use_function): print(f"007: class_use_function={class_use_function} 錯誤示范,類級不可以調用函數級") def test_008(self, function_use_class): print(f"008: function_use_class 調用函數級-->函數級再調用類級login,運行過了不會再運行") def test_005(self): print(f"005: 未傳入任何fixture,哪個級別都與我無關") class TestNoFixture: def test_009(self): print('009:未傳入任何fixture,哪個級別都與我無關') if __name__ == '__main__': pytest.main()
3.scope=module 模塊級
1.調用方式:
和function一樣
2.運行時機:
每一個.py文件調用一次,該模塊內最先執行的調用了該fixture的用例執行前運行且只運行一次(如test_006 + test_001 + test_003)
2.fixture相互調用規則
1)類級可以調用模塊級,參考test_005
2)函數級可以調用模塊級,參考test_008
3)模塊級不可以調用函數級,和類級參考 test_004 test_007
代碼:

import pytest @pytest.fixture(scope='module') def open(): print("打開電腦 -- 模塊級別fixture") return 'windows' @pytest.fixture(scope='class') def login(): print("打開瀏覽器 -- 類級別fixture") return 'chrome' @pytest.fixture() def logout(): print("關閉瀏覽器 -- 函數級fixture") @pytest.fixture(scope='module') def module_use_class(login): print(f"錯誤示范,模塊級不可以調用類級") return f'{login}' @pytest.fixture(scope='module') def module_use_func(logout): print(f"錯誤示范,模塊級不可以調用函數級") return f'{logout}' @pytest.fixture() def function_use_module(open): print(f"function_use_module -- 函數級調用模塊級") @pytest.fixture(scope='class') def class_use_module(open): print(f"class_use_module -- 類級調用模塊級") def test_006(login): print(f'006: login={login}') class TestLogin: def test_001(self, open): print(f"001: open={open} 調用模塊級") def test_002(self, login): print(f"002:login={login} 調用類級") def test_003(self, open, login, logout): print(f"003: open={open} login={login} logout={logout} 調用模塊級、類級和函數級") @pytest.mark.usefixtures("module_use_class") def test_004(self): print(f"004: module_use_class 錯誤示范,模塊級不能調用類級") def test_007(self, module_use_func): print(f"007: module_use_func 錯誤示范,模塊級不能調用函數級 ") def test_008(self, function_use_module): print(f"008: function_use_module") def test_005(self, class_use_module): print(f"005: class_use_module") class TestNoFixture: def test_009(self): print('009:未傳入任何fixture,哪個級別都與我無關') if __name__ == '__main__': pytest.main()
4.scope=session
fixture為session級別是可以跨.py模塊調用的,運行一次程序只會調用一次
也就是當我們有多個.py文件的用例的時候,如果多個用例只需調用一次fixture,那就可以設置為scope="session",並且寫到conftest.py文件里作為全局的fixture
conftest.py 點擊查看文件名稱時固定的,pytest會自動識別該文件。
放到項目的根目錄下就可以全局調用了,如果放到某個package下,那就在該package內有效
三、其他參數介紹
1.params
一個可選的參數列表,它將導致被fixture裝飾的測試用例以列表中每個列表項為參數,多次調用fixture功能
1.fixture可以帶參數,params支持列表;
2.默認是None;
3.對於param里面的每個值,fixture都會去調用執行一次,就像執行for循環一樣把params里的值遍歷一次。
在 pytest 中有一個內建的 fixture 叫做 request,代表 fixture 的調用狀態。request 有一個字段 param,使用類似@pytest.fixture(param=tasks_list)
的方式給fixture傳參,在 fixture 中使用 request.param
的方式作為返回值供測試函數調用。其中 tasks_list 包含多少元素,該 fixture 就會被調用幾次,分別作用在每個用到的測試函數上
如下fixture和測試用例都執行三遍相當三個測試用例,場景如:測試三組賬戶密碼登錄,登陸用例都是一個只是每次數據不同
import pytest tasks_list = [(10,11),(20,22),(33,33)] @pytest.fixture(params=tasks_list) def test_data(request): print(f"fixture得到:賬號:{request.param[0]},密碼:{request.param[1]}" ) return request.param class TestData: def test_1(self,test_data): print("用例:",test_data) if __name__ == '__main__': pytest.main()
2.ids
ids通常可以與params一起使用,在沒有指定 id情況下,在輸出時 pytest 會自動生成一個標識作為測試ID:
- 當params列表項是數字、字符串、布爾值和None時,將使用列表項自身字符串形式表示測試ID,如[True1] [True2] [xxoo] [123] [None]等
- 對於其他對象,pytest會根據參數名稱創建一個字符串,如params中截圖顯示的[test_data0] [test_data1]
- 可以通過使用
ids
關鍵字參數來自定義用於指定測試ID,例如@pytest.fixture(param=tasks_list,ids=task_ids) , ids可以是列表,也可以是函數供 pytest 生成 task 標識。
import pytest data_list = [(10,11),(20,22),(33,33)] @pytest.fixture(params=data_list, ids=["a","b","c"]) def tes_data(request): print(f"fixture得到:賬號:{request.param[0]},密碼:{request.param[1]}" ) return request.param class TestData: def test_1(self,tes_data): print("用例:",tes_data) if __name__ == '__main__': pytest.main()
3.autouse
默認False不開啟
當用例很多的時候,每次都傳fixture,會很麻煩。fixture里面有個參數autouse,默認是False沒開啟的,可以設置為True開啟自動使用fixture功能,這樣用例就不用每次都去傳參了
autouse設置為True,自動調用fixture功能,無需傳仍何參數,作用范圍跟着scope走(謹慎使用)

import pytest @pytest.fixture(scope='module', autouse=True) def test1(): print('開始執行module') @pytest.fixture(scope='class', autouse=True) def test2(): print('開始執行class') @pytest.fixture(scope='function', autouse=True) def test3(): print('開始執行function') def test_a(): print('---用例a執行---') def test_d(): print('---用例d執行---') class TestCase: def test_b(self): print('---用例b執行---') def test_c(self): print('---用例c執行---')

========================================================================================================================= test session starts ========================================================================================================================== platform win32 -- Python 3.8.0, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- E:\python3.8\python.exe cachedir: .pytest_cache rootdir: D:\代碼\自動化測試\pytest_test, configfile: pytest.ini, testpaths: ./dir01/dir01_test.py collected 4 items dir01/dir01_test.py::test_a 開始執行module 開始執行class 開始執行function ---用例a執行--- PASSED dir01/dir01_test.py::test_d 開始執行class 開始執行function ---用例d執行--- PASSED dir01/dir01_test.py::TestCase::test_b 開始執行class 開始執行function ---用例b執行--- PASSED dir01/dir01_test.py::TestCase::test_c 開始執行function ---用例c執行--- PASSED ================================================================================================================================ PASSES ================================================================================================================================
4.name
- fixture的重命名
- 默認為 fixture 裝飾的的函數名,但是 pytest 也允許將fixture重命名
- 如果使用了name,只能將name傳入,函數名不再生效
四、fixture的teardown后置操作
1.使用yield實現
前幾章中都是前置操作,后置操作需要用到python的 yield來實現,yield語法講解點這里:>>迭代器生成器<<
- 如果yield前面的代碼,即setup部分已經拋出異常了,則不會執行yield后面的teardown內容
- 如果測試用例拋出異常,yield后面的teardown內容還是會正常執行
import pytest @pytest.fixture(scope="session") def open(): # 整個session前置操作setup print("===打開瀏覽器===") test = "測試變量是否返回" yield test # 整個session后置操作teardown print("==關閉瀏覽器==") @pytest.fixture def login(open): # 方法級別前置操作setup print(f"輸入賬號,密碼先登錄{open}") name = "==我是賬號==" pwd = "==我是密碼==" age = "==我是年齡==" # 返回變量 yield name, pwd, age # 方法級別后置操作teardown print("登錄成功") def test_s1(login): print("==用例1==") # 返回的是一個元組 print(login) # 分別賦值給不同變量 name, pwd, age = login print(name, pwd, age) assert "賬號" in name assert "密碼" in pwd assert "年齡" in age def test_s2(login): print("==用例2==") print(login) if __name__ == '__main__': pytest.main()
2.使用request.addfinalizer終結函數實現
yield是返回數據並暫停,后置操作在yield后面。
終結函數是用return返回,終結函數的后置操作處於前置和return中間。
- 如果request.addfinalizer()前面的代碼,即setup部分已經拋出異常了,則不會執行request.addfinalizer()的teardown內容(和yield相似,應該是最近新版本改成一致了)
- 可以聲明多個終結函數並調用
import pytest @pytest.fixture(scope="module") def test_addfinalizer(request): # 前置操作setup print("==打開瀏覽器==") test = "test_addfinalizer" def fin(): # 后置操作teardown print("==關閉瀏覽器==") request.addfinalizer(fin)
# 返回前置操作的變量 return test def test_anthor(test_addfinalizer): print("==最新用例==", test_addfinalizer) if __name__ == '__main__': pytest.main()