在pytest中加入fixture的目的是提供一個固定的基準,使測試能夠可靠、重復(fù)地執(zhí)行,pytest的fixture比傳統(tǒng)xUnit風(fēng)格的setup/teardown函數(shù)相比,有了巨大的改進:
- fixture具有明確的名稱,并通過在測試函數(shù)、模塊、類或整個項目中聲明它們的使用來激活。
- fixture是以模塊化的方式實現(xiàn)的,因為每個fixture名稱都會觸發(fā)fixture函數(shù),其本身也可以使用其他fixture。
- fixture管理從簡單的單元擴展到復(fù)雜的函數(shù)測試,允許根據(jù)配置和組件選項參數(shù)化fixture和測試,或者在函數(shù)、類、模塊或整個測試會話范圍內(nèi)重復(fù)使用fixture。
另外,pytest會繼續(xù)支持經(jīng)典的xUnit風(fēng)格的設(shè)置,我們可以混合使用這兩種風(fēng)格,按照自己的喜好,從經(jīng)典風(fēng)格逐漸過渡到新風(fēng)格。我們也可以從現(xiàn)有的unittest.TestCase風(fēng)格或基于nose的項目開始。
將fixture作為函數(shù)參數(shù)
測試函數(shù)可以通過將它們命名為輸入?yún)?shù)來接收fixture對象,對于每一個參數(shù)名稱而言,具有該名稱的fixture函數(shù)提供fixture對象,fixture的函數(shù)是通過用@pytest.fixture
標記來注冊的。現(xiàn)在讓我們來寫一個簡單的測試模塊,包含一個fixture和一個使用它的測試函數(shù),新建一個test_smtpsimple.py文件,輸入以下代碼:
import pytest
@pytest.fixture
def smtp():
import smtplib
return smtplib.SMTP("smtp.qq.com", 587, timeout=5)
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert 0
在上面代碼中,測試test_ehlo需要smtp
fixture的值,pytest會發(fā)現(xiàn)并調(diào)用@pytest.fixture
標記的smtp
fixture功能,運行測試如下所示:
在失敗回溯中,我們可以看到測試函數(shù)是用smtp
參數(shù)調(diào)用的,smtplib.SMTP()
實例是由fixture函數(shù)創(chuàng)建的,下面是Pytest用這種方法調(diào)用測試函數(shù)的過程:
- 由于
test_
前綴,pytest找到了測試函數(shù)test_ehlo
,測試函數(shù)需要一個名為smtp
的函數(shù)參數(shù),通過尋找一個名為smtp
的fixture標記功能來發(fā)現(xiàn)匹配的fixture函數(shù)。 - 調(diào)用
smtp()
來創(chuàng)建一個實例。 -
test_ehlo(<SMTP instance>)
被調(diào)用,并在測試函數(shù)的最后一行失敗。
需要注意的是,如果拼錯一個函數(shù)參數(shù)或想使用一個不可用的函數(shù)參數(shù),將會看到一個包含可用函數(shù)參數(shù)列表的錯誤。我們可以使用以下命令看到可用的fixture:
pytest --fixtures test_simplefactory.py
使用fixture的依賴注入
fixture允許測試函數(shù)輕松接收和處理特定的,預(yù)先初始化的應(yīng)用程序?qū)ο螅槐卦谝鈱?dǎo)入、設(shè)置、清理的細節(jié)。這是fixture依賴注入的主要做法,fixture函數(shù)是注入器的角色,測試函數(shù)是fixture對象的消費者。
如果在執(zhí)行測試期間,我們意識到要使用來自多個測試文件的fixture函數(shù),您可以將其移至conftest.py
文件。這樣一來,我們就不需要導(dǎo)入想在測試中使用的fixture,此時它會自動被pytest發(fā)現(xiàn)。pytest函數(shù)的發(fā)現(xiàn)始于測試類,然后是測試模塊,再然后是conftest.py
文件,最后是內(nèi)置和第三方插件。
如果我們想從測試中獲得可用的測試數(shù)據(jù),有一個好方法是把這些數(shù)據(jù)加載到我們的測試中使用,這是pytest的自動緩存機制。或者,另一個好方法是在測試文件夾中添加數(shù)據(jù)文件。還有一些社區(qū)插件可以幫助我們管理這方面的測試,例如pytest-datadir和pytest-datafiles。
在指定范圍內(nèi)共享fixture實例
需要網(wǎng)絡(luò)訪問的fixture取決于連接性,而且創(chuàng)建起來往往花費大量時間,擴展前面的例子,我們可以在@pytest.fixture
調(diào)用中添加一個scope='module'
參數(shù),使每個測試模塊只能調(diào)用一次修飾的smtp
fixture函數(shù),默認是每個測試函數(shù)調(diào)用一次。測試模塊中的多個測試函數(shù)因此將分別接收相同的smtp
fixture實例,從而節(jié)省時間。
在下面的示例中,我們將fixture函數(shù)放入一個單獨的conftest.py
文件中,以便目錄中多個測試模塊的測試都可以訪問fixture函數(shù),新建一個conftest.py文件,輸入以下代碼:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp():
return smtplib.SMTP("smtp.qq.com", 587, timeout=5)
fixture的名字還是smtp
,我們可以通過列出名稱smtp
作為輸入?yún)?shù)在任何測試或fixture函數(shù)來訪問其結(jié)果,前提是位于或低于conftest.py
所在的目錄,新建一個test_module.py文件,輸入以下代碼:
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert b"smtp.qq.com" in msg
assert 0
def test_noop(smtp):
response, msg = smtp.noop()
assert response == 250
assert 0
我們故意插入失敗的assert 0
語句來檢查正在發(fā)生的事情,現(xiàn)在可以運行測試:
我們可以看到兩個assert 0
失敗,更重要的是,我們也可以看到同一模塊范圍內(nèi)的smtp
對象被傳遞到兩個測試函數(shù)中,因為pytest顯示了回溯中的傳入?yún)?shù)值。因此,使用smtp
的兩個測試函數(shù)的運行速度與單個測試函數(shù)一樣快,因為它們重用了相同的實例。
如果我們希望擁有一個會話范圍的smtp
實例,則可以簡單地聲明它,這樣的話,類范圍將在每個測試類中調(diào)用一次fixture:
@pytest.fixture(scope="session")
def smtp(...):
fixture的完成與拆卸代碼
當fixture超出范圍時,pytest支持執(zhí)行fixture特定的最終代碼,通過使用yield
語句而不是return
,yield
語句之后的所有代碼都用作拆卸代碼,修改conftest.py
的代碼:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp():
smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
yield smtp
print("拆卸smtp")
smtp.close()
print
和smtp.close()
語句將在模塊的最后一次測試完成后執(zhí)行,不管測試的異常狀態(tài)如何,讓我們來執(zhí)行它:
我們看到,在兩個測試完成執(zhí)行后,smtp
實例已經(jīng)完成,需要注意的是,如果我們使用scope='function'
來修飾fixture函數(shù),那么fixture設(shè)置和清理將在每個單獨的測試中發(fā)生。在任何一種情況下,測試模塊本身都不需要改變或了解fixture設(shè)置的這些細節(jié)。
同樣,我們也可以通過with
語句無縫地使用yield
語法,這樣測試完成后,smtp
連接將被關(guān)閉,因為當with
語句結(jié)束時,smtp
對象會自動關(guān)閉:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp():
with smtplib.SMTP("smtp.qq.com", 587, timeout=5) as smtp:
yield smtp
需要注意一下,如果在設(shè)置代碼,即yield
關(guān)鍵字之前,期間發(fā)生異常,則不會調(diào)用拆卸代碼,即即yield
關(guān)鍵字之后的代碼。執(zhí)行拆卸代碼的另一種選擇是利用請求上下文對象的addfinalizer
方法來注冊完成函數(shù)。例如,下面的smtp
fixture更改為使用addfinalizer
進行清理:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
def fin():
print ("拆卸smtp")
smtp.close()
request.addfinalizer(fin)
return smtp
yield
和addfinalizer
方法在測試結(jié)束后通過調(diào)用它們的代碼來工作,但addfinalizer
與yield
相比有兩個關(guān)鍵的區(qū)別。第一點是,可以注冊多個完成函數(shù)。第二點是,無論fixture設(shè)置代碼是否引發(fā)異常,完成函數(shù)將始終被調(diào)用,即使其中一個未能創(chuàng)建與獲取,也可以正確關(guān)閉由fixture創(chuàng)建的所有資源:
@pytest.fixture
def equipments(request):
r = []
for port in ('C1', 'C3', 'C28'):
equip = connect(port)
request.addfinalizer(equip.disconnect)
r.append(equip)
return r
在上面的代碼中,如果“C28”發(fā)生異常,“C1”和“C3”仍然會被正確關(guān)閉。當然,如果在完成函數(shù)注冊之前發(fā)生異常,那么它將不會被執(zhí)行。
fixture反向獲取請求的測試環(huán)境
fixture函數(shù)可以通過接受request
對象來反向獲取請求中的測試函數(shù)、類或模塊上下文,進一步擴展之前的smtp
fixture示例,讓我們從fixture的測試模塊讀取可選的服務(wù)器URL:
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp(request):
server = getattr(request.module, "smtpserver", "smtp.qq.com")
smtp = smtplib.SMTP(server, 587, timeout=5)
yield smtp
print ("完成 %s (%s)" % (smtp, server))
smtp.close()
我們使用request.module
屬性來從測試模塊中選擇性地獲取smtpserver
屬性,如果我們再次執(zhí)行,沒有什么改變:
再讓我們快速創(chuàng)建另一個測試模塊,在其模塊名稱空間中實際設(shè)置服務(wù)器URL,新建一個test_anothersmtp.py文件,輸入以下代碼:
smtpserver = "mail.python.org"
def test_showhelo(smtp):
assert 0, smtp.helo()
然后我們運行它:
大家可以看到,smtp
fixture函數(shù)從模塊名稱空間中選取我們的郵件服務(wù)器名稱。
參數(shù)化fixture
fixture函數(shù)可以參數(shù)化,在這種情況下,它們將被多次調(diào)用,每次執(zhí)行一組相關(guān)測試,即依賴于這個fixture的測試,測試函數(shù)通常不需要知道它們的重新運行。fixture參數(shù)化可以用于一些有多種方式配置的功能測試。
擴展前面的例子,我們可以標記fixture來創(chuàng)建兩個smtp
fixture實例,這將導(dǎo)致使用fixture的所有測試運行兩次,fixture函數(shù)通過特殊的request
對象訪問每個參數(shù):
import pytest
import smtplib
@pytest.fixture(scope="module",
params=["smtp.qq.com", "mail.python.org"])
def smtp(request):
smtp = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp
print ("完成 %s" % smtp)
smtp.close()
主要的變化是使用@pytest.fixture
聲明params
,這是fixture函數(shù)將執(zhí)行的每個值的列表,并且可以通過request.param
訪問一個值,沒有測試函數(shù)代碼需要改變,所以讓我們執(zhí)行一次:
我們可以看到,我們的兩個測試函數(shù)每個都運行了兩次,而且是針對不同的smtp
實例。pytest將建立一個字符串,它是參數(shù)化fixture中每個fixture值的測試ID,在上面的例子中,test_ehlo[smtp.qq.com]
和test_ehlo[mail.python.org]
,這些ID可以與-k
一起使用來選擇要運行的特定實例,還可以在發(fā)生故障時識別特定實例。使用--collect-only
運行pytest會顯示生成的ID。
數(shù)字、字符串、布爾值和None將在測試ID中使用其通常的字符串表示形式,對于其他對象,pytest會根據(jù)參數(shù)名稱創(chuàng)建一個字符串,可以通過使用ids
關(guān)鍵字參數(shù)來自定義用于測試ID的字符串。新建一個test_ids.py文件,輸入以下代碼:
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
上面顯示了ids
可以是一個要使用的字符串列表,還可以是一個將用fixture值調(diào)用的函數(shù),然后返回一個字符串來使用。在后一種情況下,如果函數(shù)返回None
,那么將使用pytest的自動生成的ID。運行上述測試會導(dǎo)致使用以下測試ID: