通過之前一系列的解讀,相信大家對于fixture已經有了更多的理解,fixture功能強大,
我覺得用來處理setup、teardown非常的靈活,好用,
但是,畢竟它也只是一段程式代碼,雖然可以幫我們做setup、teardown的處理,但是并不代表任何情況下都可以完美處理掉,
拿teardown來說,假如我們寫的代碼不小心報錯了,導致該刪掉的沒刪掉,那么就可能會導致后續一些奇怪的問題發生,
一、不可靠的fixture函式長啥樣?
一起先來看下官方給的代碼示例(文末會用我自己的實踐代碼來表述核心思想):
import pytest
from emaillib import Email, MailAdminClient
@pytest.fixture
def setup():
mail_admin = MailAdminClient()
sending_user = mail_admin.create_user()
receiving_user = mail_admin.create_user()
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_emai(email, receiving_user)
yield receiving_user, email
receiving_user.delete_email(email)
admin_client.delete_user(sending_user)
admin_client.delete_user(receiving_user)
def test_email_received(setup):
receiving_user, email = setup
assert email in receiving_user.inbox
這段代碼你別急著copy過去運行,因為我試過不行,我們主要用它來輔助描述一些概念,
先來看下這段代碼是干嘛使的:
- 這是一個測驗用例代碼,測驗用戶之間收發郵件的,
test_email_received是測驗函式,并且用了一個叫setup的fixture函式,setup就是這個fixture函式,yield之前的代碼主要是用來創建2個用戶,一個是發送者,另一個是接收者,
并且發送了郵件yield之后的代碼就是在測驗結束后,接收者洗掉郵件,客戶端洗掉這2個測驗用戶,
思路清晰,似乎沒啥問題,但是,這里的fixture函式結構就是一個典型的不太可靠的案例,為什么?
- 首先,
setup這個fixture名稱缺乏描述性,當然這不是最重要的問題, - 在
setup這一個fixture函式中做的事情太多,很多步驟不容易重用, - 最嚴重的的問題來了,如果yield之前的任何代碼報錯,那么yield之后的teardown代碼都不會運行,
這就是會出現本章開頭提到的問題,有些資料被生成出來,最后沒有被洗掉掉,
雖然,在之前我們也學到了用addfinalizer來處理,可以讓teardown代碼繼續執行,但是不得不說,那種寫法還是
比較復雜的,而且閱讀性跟維護性都不是很好,
二、如何讓fixture函式更可靠
其實很多事情要想可靠,首先必須要簡單,
上面的fixture不是一個里面做的事情太多了嗎?那么就把他們都拆出來,用的時候再把他們重新系結在一起就好了,
1、官方示例代碼1
對于上面發送郵件的測驗代碼,就可以改成下面這種:
import pytest
from emaillib import Email, MailAdminClient
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
admin_client.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
admin_client.delete_user(user)
def test_email_received(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
可以看出,每個fixture函式里只做一種狀態的操作,
比如sending_user里,就只做發送者的創建跟洗掉,receiving_user里就只做接收者的創建跟洗掉,mail_admin是用來生成一個類似管理員的
客戶端,用來創建用戶,也把它獨立成一個fixture函式,用的時候就可以跟另外2個系結在一起使用了,
看到這可能還有點不明白,沒關系,繼續看下一個官方示例,
2、官方示例代碼2
這是一個web自動化的測驗用例,
假如,我們有一個登錄頁面,需要進行登錄測驗,為了方便測驗,我們還有一個管理員的api,可以直接呼叫來生成測驗用戶,
那么,這個測驗場景通常會這樣去構建:
- 通過管理API創建一個用戶
- 使用Selenium啟動瀏覽器
- 進入我們網站的登錄頁面
- 使用創建好的用戶進行登錄
- 斷言登錄后的用戶名出現在登錄頁的頁眉中
于是乎,測驗代碼也就有了(注:依然是不能copy運行的代碼,假設代碼中所有需依賴的代碼都存在):
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture
def login(driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
@pytest.fixture
def landing_page(driver, login):
return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
可以看出,測驗代碼的結構就是按照上述的思路分析來進行構造的,
這種布局可能乍一看,你并不清楚user和driver這2個fixture函式哪個是先執行的,但是沒關系,他們一定是有一個
先執行一個后執行的,之前的文章里也講過了執行順序,不清楚的可以往前翻看我的文章,
但是,這都不是重點,重點在于,我不管這2個fixture誰先運行,如果其中誰報錯了,那么這2個fixture函式都不留下任何東西,
- 如果
driver在user之前運行,但是user報錯了,
那么,driver里的teardown依然會執行,瀏覽器驅動會退出,并且,因為user報錯了,所以測驗用戶并沒有被創建出來, - 如果,
driver運行的時候就報錯了,那么瀏覽器驅動都不會進行初始化,而運行順序排后面到user根本都不會去運行了,
所以也就更不會生成測驗用戶了,
3、我自己的實戰代碼
是不是有點眉目了?但是還是不能徹底理解?沒關系,我在專案的實戰代碼中就是這么用的,可以直接拿來驗證效果,
這里我貼出來2個fixture函式,有著呼叫的關系,測驗case的代碼就不用貼了,
做的事情也很簡單,就是往兩個關聯表的插資料,第一個是主表,另一個是附屬表,
在yield之前是setup操作,負責插入資料,在yield之后是teardown操作,負責洗掉資料,
在format部分,我會做手腳分別讓這2個fixture函式報錯,來看下互相的影響,
@pytest.fixture()
def insert_sm_purchase_order():
"""
插入采購訂單表的資料
:return:
"""
db = DB("db_info")
purchase_order_sn = "CGN016" + deal_date() + str(random_int(4))
purchase_order_id = db.get_table_usable_latest_id("tcwms", "sm_purchase_order")
insert_sql = """
INSERT INTO `tcw`
...
sql陳述句直接省略了
""".format(purchase_order_id, purchase_order_sn, C_TIME, int(C_TIME))# 這里讓其報錯
db.exec_sql(insert_sql)
db.close()
yield purchase_order_id, purchase_order_sn
db = DB("db_info")
db.exec_sql("DELETE FROM `tcwm... sql陳述句直接省略了)
db.close()
@pytest.fixture()
def insert_sm_purchase_order_goods(insert_sm_purchase_order):
"""
插入采購商品表的資料
:return:
"""
db = DB("db_info")
purchase_order_goods_id = db.get_table_usable_latest_id("tcwms", "sm_purchase_order_goods")
po_id = insert_sm_purchase_order[0]
purchase_order_sn = insert_sm_purchase_order[1]
insert_sql = """
INSERT INTO `tcw`
...
sql陳述句直接省略了
""".format(purchase_order_goods_id, po_id, C_TIME)# 這里讓其報錯
db.exec_sql(insert_sql)
db.close()
yield purchase_order_goods_id, po_id, purchase_order_sn
db = DB("tcwms_db_info")
db.exec_sql("DELETE FROM `tcwm... sql陳述句直接省略了)
db.close()
這里2個fixture從上到下,姑且叫做fixture1和fixture2吧,運行順序是 fixture1先,
- 我先讓fixture2報錯,按理說,fixture1可以正常插入洗掉,fixture2報錯了也就是插入資料失敗了,
看下運行結果:


- 接著,我恢復fixture2,再讓fixture1報錯,這時候,應該是fixture1直接setup就報錯了,故fixture2也就不會再執行了,
看下運行結果:

所以說,現在你明白了嗎?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/271850.html
標籤:其他
