pytest進階之fixture函式

江楓對愁眠 發表於 2021-03-27

fixture函式存在意義

  與python自帶的unitest測試框架中的setup、teardown類似,pytest提供了fixture函式用以在測試執行前和執行後進行必要的準備和清理工作。但是相對來說又比setup、teardown好用。

firture相對於setup和teardown的優勢

  1. 命名方式靈活,不侷限於setup和teardown這幾個命名
  2. conftest.py 配置裡可以實現資料共享,不需要import就能自動找到一些配置
  3. scope="module" 可以實現多個.py跨檔案共享前置, 每一個.py檔案呼叫一次
  4. scope="session" 以實現多個.py跨檔案使用一個session來完成多個用例

fixture函式定義

  通過將fixture宣告為引數名,測試用例函式可以請求fixture。fixture修飾器來標記固定的工廠函式,在其他函式,模組,類或整個工程呼叫它時會被啟用並優先執行,通常會被用於完成預置處理和重複操作
 
 1 # 定義的夾具函式,使用裝飾器pytest.fixture
 2 @pytest.fixture
 3 def my_fruit():
 4     print("login:使用者執行登入操作")
 5 
 6 # 使用夾具函式的測試用例
 7 def test_my_fruit_in_basket(my_fruit):
 8     print("hello world")
 9 
10 if __name__ == '__main__':
11     pytest.main(['test_login.py::test_my_fruit_in_basket', '-s'])
12     
13 #執行結果:
14 collected 1 item 
15 test_login.py login:
16 使用者執行登入操作 
17 hello world 
18 . 
19 ============================== 1 passed in 0.02s ==========================

fixture作用

  1. 做測試前後的初始化設定,如測試資料準備,連結資料庫,開啟瀏覽器等這些操作都可以使用fixture來實現。
  2. 測試用例的前置條件可以使用fixture實現 。
  3. 支援經典的xunit fixture ,像unittest使用的setup和teardown。
  4. fixture可以實現unittest不能實現的功能,比如unittest中的測試用例和測試用例之間是無法傳遞引數和資料的,但是fixture卻可以解決這個問題。

呼叫fixture有三種方式

  1. Fixture名字作為測試用例的引數
  可以直接使用fixture名稱作為輸入引數(是個典型的高階函式),在這種情況下,fixture函式返回的fixture例項將被注入,最終在測試用例執行前執行這個裝飾過的函式。如下列程式碼,①將返回值傳遞給測試用例,②通過函式入參方式,可以傳入多個fixture函式
 1 import pytest
 2 
 3 @pytest.fixture
 4 def first_entry():
 5     return "a"
 6 @pytest.fixture
 7 def order(first_entry):
 8     return [first_entry]
 9 def test_string(order):
10     order.append("b")
11     assert order == ["a", "b"], "斷言執行失敗"
12  
13 if __name__ == '__main__':
14     pytest.main(['test_login.py::test_string', '-s'])
  1. 使用@pytest.mark.usefixtures('fixture')裝飾器
   每個函式或者類前使用@pytest.mark.usefixtures('fixture')裝飾器進行裝飾。
import pytest
@pytest.fixture
def my_fruit():
    print("login:使用者執行登入操作")

# 被夾具函式裝飾的測試用例
@pytest.mark.usefixtures("my_fruit")
def test_my_fruit_in_basket():
    print("hello world")


if __name__ == '__main__':
    pytest.main(['test_login.py', '-s', '-q'])
    
# 執行結果
login:使用者執行登入操作 
hello world 
. 
1 passed in 0.01s
  1. 使用autouse引數
  指定fixture的引數autouse=True這樣模組內的每個測試用例會自動呼叫fixture。
import pytest
@pytest.fixture(autouse=True)
def my_fruit():
    print("login:使用者執行登入操作")

# 被夾具函式裝飾的測試用例
def test_my_fruit_in_basket():
    print("hello world")


if __name__ == '__main__':
    pytest.main(['test_login.py', '-s', '-q'])
備註: 如果fixture有返回值,那麼usefixture以及autouse就無法獲取到返回值,這個是裝飾器usefixture與用例直接傳fixture引數的區別。 因此最常用的是通過引數傳遞的方法。

指定Fixture函式的作用範圍

Fixture中的scope的引數,控制Fixture函式的作用範圍
scope = ‘function’ 測試函式維度,預設範圍,則在測試結束時銷燬fixture。
scope = ‘class’ 測試類維度,在class中最後一次測試的拆卸過程中,夾具被破壞。
scope = ‘module’ 測試檔案維度,在模組中最後一次測試的拆卸過程中,夾具被破壞。
scope = ‘session’ 測試會話維度,夾具在測試會話結束時被銷燬。

fixture函式的返回值:return 和 yield 和 addfinalizer終結函式

return:

  通過下面的程式碼,我們已經發現可以通過測試用例函式傳入引數的形式,直接使用fixture函式的返回值,這個相對來說比較簡單。
import pytest

@pytest.fixture
def first_entry():
    return "a"
@pytest.fixture
def order(first_entry):
    return [first_entry]
def test_string(order):
    order.append("b")
    assert order == ["a", "b"], "斷言執行失敗"
 
if __name__ == '__main__':
    pytest.main(['test_login.py::test_string', '-s'])

yield:

  yeild也是一種函式的返回值型別,是函式上下文管理器,使用yield被調fixture函式執行遇到yield會停止執行,接著執行呼叫的函式,呼叫的函式執行完後會繼續執行fixture函式yield關鍵後面的程式碼。因此利用fixture函式,我們可以說pytest集合了setup、teardown,既做了初始化,又做了後置的清理工作。
import pytest
from emaillib import Email, MailAdminClient

@pytest.fixture
def mail_admin():
    return MailAdminClient()

# 配置傳送者的fixture函式
@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user() #setup:建立發件人
    yield user                      # 返回發件人
    admin_client.delete_user(user)  #teardown:刪除發件人

# 配置收件人的fixture函式
@pytest.fixture
def receiving_user(mail_admin):
    user = mail_admin.create_user()  #setup:建立收件人
    yield user                       #teardown:返回收件人
    admin_client.delete_user(user)  #teardown:刪除收件人

def test_email_received(sending_user, receiving_user, email):
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(email, receiving_user)
    assert email in receiving_user.inbox

專案中的實際使用

  翻譯下面程式碼,在呼叫Entry_into_index前,啟動APP,遇到yield關鍵字,中止fixture函式呼叫,執行呼叫函式Entry_into_index內容,在Entry_into_index函式呼叫後,執行yield函式後的driver.close_app(),關閉APP
@pytest.fixture(scope='session')
def startApp_fixture(start_app):
    driver = start_app
    res = lp(driver).get_agree_info()
    try:
        assert res == "同意"
    except Exception as e:
        log.error("啟動APP失敗")
        log.exception(e)
        raise e
    else:
        lp(driver).click_agree()
        lp(driver).click_next_step()
        lp(driver).click_alert()
        lp(driver).click_pass()
        # 建立首頁
        index_page = indexPage(driver)
        yield index_page, driver
        # 後置條件
        time.sleep(3)
        driver.close_app()
        
# 呼叫fixture函式
@pytest.fixture(scope='session')
def Entry_into_index(startApp_fixture)
    index_page = startApp_fixture()[0]
    driver = startApp_fixture()[1]

fixture函式需要傳遞引數

工廠作為固定裝置:可以使用閉包,通過外部去呼叫函式裡面函式。
工廠固定裝置原因:
上面已經說過,呼叫fixture函式A可以通過用fixture名稱作為呼叫函式B引數,在這種情況下,fixture函式返回的fixture例項將被注入,最終在測試用例B執行前執行這個裝飾過的函式def B(A):pass。但是有個問題在給測試用例新增裝飾函式時,傳入的引數是fixture函式的函式名,如果需要給fixture函式新增引數時,是不可以用下面形式,程式碼會直接報錯。原因是測試用例傳入引數為fixture函式名,如果fixture函式名新增(引數)後,表現形式為add(params)實際為函式呼叫。可參考高階函式與裝飾器,並無此用法。
pytest進階之fixture函式
 
 
 
 
 
 
 
 
 
解決方式使用閉包,如下圖程式碼:make_customer_record函式返回的是內部函式_make_customer_record(夾具不直接返回資料,而是返回一個生成資料的函式),注意此處未加(),非函式呼叫,因此在測試用例中customer_1 = make_customer_record("Lisa")此處可拆解為兩部分,customer_1 = make_customer_record的結果為_make_customer_record物件 ,加上("Lisa") 實際是對調_make_customer_record函式進行呼叫:函式名+(引數),以達到可以傳參的目的。
@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name": name, "orders": []}

    return _make_customer_record   #注意此處不加(),非函式呼叫

def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")