[跟著官方文檔學pytest][六][fixture][學習筆記]
1.teardown/cleanup(也叫fixture finalization)
當我們運行我們的測試時,希望確保它們自己清理干凈,這樣它們就不會與任何其他測試相混淆(同時也不會留下大量的測試數據使得系統膨脹)。pytest中的fixtures提供了一個非常有用的teardown系統,允許我們定義每個fixtures自行清理的特定步驟。
該系統可以通過兩種方式加以利用。
1.1.yield fixtures(建議)
"Yield"fixtures yield而不是return。使用這些fixtures,我們可以運行一些代碼并將對象傳遞回請求fixture/測試,就像其他fixtures一樣。唯一的區別是:
return換成yield。
該fixtures的任何teardown代碼都放在yield之后。
一旦pytest計算出fixtures的線性順序,它將運行每個fixture直到return或者yield,然后移動到列表中的下一個fixture做同樣的事情。
測試完成后,pytest將返回fixture列表,但順序相反,取出每個yielded,并在其中運行yield語句之后的代碼。
以下是一個簡單的電子郵件模塊:
# content of emaillib.py class MailAdminClient: def create_user(self): return MailUser() def delete_user(self): # do some cleanup pass class MailUser: def __init__(self): self.inbox=[] def send_mail(self,email,other): other.inbox.append(email) def clear_mailbox(self): self.inbox.clear() class Email: def __init__(self,subject,body): self.subject=subject self.body=body
假設我們要測試從一個用戶向另一個用戶發送電子郵件。 我們必須首先創建每個用戶,然后將電子郵件從一個用戶發送給另一個用戶,最后斷言另一個用戶在他們的收件箱中收到了該郵件。 如果我們想在測試運行后進行清理,我們可能必須確保其他用戶的郵箱在刪除該用戶之前清空,否則系統可能會報錯。
這可能是這樣的:
# content of test_emaillib.py 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 mail_admin.delete_user(user) @pytest.fixture def receiving_user(mail_admin): user = mail_admin.create_user() yield user mail_admin.delete_user(user) def test_email_received(sending_user, receiving_user): email = Email(subject="Hey!", body="How's it going?") sending_user.send_email(email, receiving_user) assert email in receiving_user.inbox
因為receiving_user時設置時最后一個fixture,所以它是第一個運行teardown的。
有一種風險,即使在teardown有正確的順序也不能保證安全清理。在safe teardown中有詳細介紹。
1.1.1.Handling errors for yield fixture
如果yield fixture在yield之前引發了異常,pytest不會嘗試在該yield fixture的yield語句之后運行teardown代碼。但是,對于已經成功運行該測試的每個fixture,pytest仍將嘗試teardown。
1.2.Adding finalizers directly
雖然yield fixture被認為是更清晰、更直接的選擇,但還有另一種選擇,即直接將"finalizer"函數添加到測試的request-context對象中。它帶來了與yield fixtures類似的結果,但更冗長。
為了使用這種方法,我們必須在需要為其添加teardown代碼的fixture中請求request-context對象(就像我們請求另一個fixture一樣),然后將包含該teardown代碼的可調用代碼傳遞給其addfinalizer方法。
但我們要小心,因為pytest將在添加該finalizer后運行,即使fixture在添加finalizer后引發異常。因此,為了確保我們不會在不需要的時候運行finalizer,我們只會在fixture完成我們需要的teardown后添加finalizer。
下面是使用addfinalizer方法運行上一個示例:
# content of test_emaillib.py 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 mail_admin.delete_user(user) @pytest.fixture def receiving_user(mail_admin, request): user = mail_admin.create_user() def delete_user(): mail_admin.delete_user(user) request.addfinalizer(delete_user) return user @pytest.fixture def email(sending_user, receiving_user, request): _email = Email(subject="Hey!", body="How's it going?") sending_user.send_email(_email, receiving_user) def empty_mailbox(): receiving_user.clear_mailbox() request.addfinalizer(empty_mailbox) return _email def test_email_received(receiving_user, email): assert email in receiving_user.inbox
2.safe teardowns
pytest的fixture系統非常強大,但仍然由計算機運行,因此它無法弄清楚如何安全進行teardown。如果我們不小心,錯誤位置的錯誤可能會給我們導致進一步的錯誤。
例如,考慮一下測試(基于上面的郵件示例):
# content of test_emaillib.py 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_email(email, receiving_user) yield receiving_user, email receiving_user.clear_mailbox() mail_admin.delete_user(sending_user) mail_admin.delete_user(receiving_user) def test_email_received(setup): receiving_user, email = setup assert email in receiving_user.inbox
這個版本更緊湊,也更難閱讀,沒有一個描述性很強的fixture名稱,而且沒有一個fixture可以重用。
還有一個更嚴重的問題,如果設置中任何步驟發生異常,則任何teardown代碼都不會運行。
還有一個更嚴重的問題,如果設置中的任何步驟引發異常,則任何teardown都不會運行。
一種選擇可能是使用addfinalizer方法而不是yield,但這可能會變得非常復雜且難以維護(而且不再緊湊)。
2.1.safe fixture structure
如上面的電子郵件示例所示,最安全和最簡單的fixture結構要求將fixture限制為每個僅執行一個狀態更改操作,然后將它們與其teardown代碼捆綁在一起。
狀態更改操作可能失敗但仍然修改狀態的可能性可以忽略不計,因為這些操作中的大多數往往是基于事務的(至少在狀態可能被拋在后面的測試級別上)。因此,如果我們通過將任何成功的狀態更改操作移動到單獨的fixture函數并將其與其他可能失敗的狀態更改操作分開來確保任何成功的狀態更改操作都被teardown。
例如,假設我們有一個帶有登錄頁面的網站,并且我們可以訪問一個可以生成用戶的管理API。對于我們的測試,我們希望:
通過admin的API創建用戶
使用Selenium啟動瀏覽器
訪問login頁面
使用我們創建的用戶登錄
在登陸頁面顯示他們的名字
我們不想讓用戶留在系統,也不想瀏覽器會話繼續運行,所以我們要確保創建這些東西的fixtures自己清理干凈。
注意:對于這個示例,某些fixtures(比如base_url和admin_credentials)暗示存在于其他地方。所以現在,我們假設它們存在,我們只是不看它們。
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}!"
依賴項的布局方式意味著尚不清楚用戶fixture是否會在驅動程序fixture之前執行。但這沒關系,因為這些都是原子操作,所以哪個先運行并不重要,因為測試的事件序列仍然是線性的。但重要的是,無論哪一個先運行,如果一個提出異常而另一個不會,那么兩者都不會留下任何東西。如果驅動程序在用戶之前執行,并且用戶引發異常,則驅動程序仍將退出,并且從未創建用戶。如果驅動程序是引發異常的驅動程序,則永遠不會啟動驅動程序,也永遠不會創建用戶。
3.Running multiple assert statements safely
有時可能希望在完成所有這些設置后運行多個斷言,這是有道理的,因為在更復雜的系統中,單個操作可以啟動多個行為。pytest有一種方便的方法來處理這個問題,它結合了我們迄今為止已經討論過的一堆內容。
所需要的只是升級到更大的范圍,然后將act步驟定義為autouse fixture,最后,確保所有fixtures都針對更高級別的范圍。
以上一個示例為例,并對其進行一些調整。假設除了檢查header中的歡迎消息外,我們還希望檢查注銷按鈕以及指向用戶配置文件的鏈接。
# contents of tests/end_to_end/test_login.py 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(scope="class") def admin_client(base_url, admin_credentials): return AdminApiClient(base_url, **admin_credentials) @pytest.fixture(scope="class") 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(scope="class") def driver(): _driver = Chrome() yield _driver _driver.quit() @pytest.fixture(scope="class") def landing_page(driver, login): return LandingPage(driver) class TestLandingPageSuccess: @pytest.fixture(scope="class", autouse=True) def login(self, driver, base_url, user): driver.get(urljoin(base_url, "/login")) page = LoginPage(driver) page.login(user) def test_name_in_header(self, landing_page, user): assert landing_page.header == f"Welcome, {user.name}!" def test_sign_out_button(self, landing_page): assert landing_page.sign_out_button.is_displayed() def test_profile_link(self, landing_page, user): profile_href = urljoin(base_url, f"/profile?id={user.profile_id}") assert landing_page.profile_link.get_attribute("href") == profile_href
注意這些方法只是在簽名中引用self作為一種形式。沒有狀態與實際測試類相關聯,因為它可能在unittest.TestCase框架中。一切都由pytest fixture系統管理。
每個方法只需要請求它實際需要的fixtures,而不用擔心順序。這是因為act fixture是一個autouse fixture,它確保所有其他fixture在它之前執行。不再需要進行狀態更改,因此測試可以根據需要自由地進行盡可能多的非狀態更改查詢,而不會冒踩到其他測試的風險。
登錄fixture也在類內部定義,因為并非模塊中的其他測試都期望成功登錄,并且對于另一個測試類可能稍微不同地處理該行為。例如,如果我們想圍繞提交錯誤編寫另一個測試場景,可以通過在測試文件中添加類似這樣的內容來處理:
class TestLandingPageBadCredentials: @pytest.fixture(scope="class") def faux_user(self, user): _user = deepcopy(user) _user.password = "badpass" return _user def test_raises_bad_credentials_exception(self, login_page, faux_user): with pytest.raises(BadCredentialsException): login_page.login(faux_user)
4.Fixtures can introspect the requeting test context
Fixture函數可以接受請求對象來內省"requesting"測試函數、類或模塊上下文。進一步擴展之前的 smtp_connection示例,讓我們從使用我們的fixture的測試模塊中讀取一個可選的服務器URL:
# content of conftest.py import pytest import smtplib @pytest.fixture(scope="module") def smtp_connection(request): server=getattr(request.module,"smtpserver","smtp.qq.com") smtp_connection=smtplib.SMTP(server,465,timeout=5) yield smtp_connection print("finalizing {} ({})".format(smtp_connection,server)) smtp_connection.close()
我們使用request.module屬性來選擇性從測試模塊獲取smtpserver屬性。
5.Using markers to pass data to fixtures
使用request對象,fixture還可以訪問用于測試函數的標記。這對于將數據從測試傳遞到fixture中非常有用。
import pytest @pytest.fixture def fixt(request): marker=request.node.get_closet_marker("fixt_data") if marker is None: # Handle missing marker is some way... data=None else: data=marker.args[0] # Do something with the data return data @pytest.mark.fixt_data(42) def test_fixt(fixt): assert fixt==42
6.Factories as fixtures
"工廠作為fixture"模式可以在單個測試中多次需要fixture結果的情況下提供幫助。fixture不是直接返回數據,而是返回一個生成數據的函數。然后,可以在測試中多次調用此函數。
import pytest @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): customer1=make_customer_record("Lisa") customer2=make_customer_record("Mike") customer3=make_customer_record("Meredith") print("\n") print(customer1)#{'name': 'Lisa', 'orders': []} print(customer2)#{'name': 'Mike', 'orders': []} print(customer3)#{'name': 'Meredith', 'orders': []}
7.Using marks with parametrized fixtures
pytest.param()可用于在參數化fixture的集合中,就像它們可以與@pytest.mark.parametrize一起使用一樣
import pytest @pytest.fixture(params=[0,1,pytest.param(2,marks=pytest.mark.skip)]) def data_set(request): return request.param def test_data(data_set): pass
運行測試將跳過對值為2的data_set的調用
============================= test session starts ============================= collecting ... collected 3 items test_fixture_marks.py::test_data[0] PASSED [ 33%] test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%] Skipped: unconditional skip ======================== 2 passed, 1 skipped in 0.01s =========================
Python
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。