pytest的fixture的詳細使用 (更靈活高級的前/后置處理方法)


一、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()進行疊加,先執行的放底層,后執行的放上層

View Code

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()
View Code

可以先畫出繼承關系圖,這樣就會很明了,按着等級去找就對了:

在實例化基本規則大前提下,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()
View Code

 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()
View Code

 

 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()
View Code

 

 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執行---')
自動使用fixture
========================================================================================================================= 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()

參考:

輝輝輝輝a

小菠蘿

全棧測試開發日記

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM