探索pytest的fixture(上)

在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的網(wǎng)絡(luò)圖片

將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功能,運行測試如下所示:

test_ehlo測試截圖

在失敗回溯中,我們可以看到測試函數(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)在可以運行測試:

test_ehlo與test_noop測試截圖

我們可以看到兩個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語句而不是returnyield語句之后的所有代碼都用作拆卸代碼,修改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()

printsmtp.close()語句將在模塊的最后一次測試完成后執(zhí)行,不管測試的異常狀態(tài)如何,讓我們來執(zhí)行它:

執(zhí)行yield語句的屏幕

我們看到,在兩個測試完成執(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

yieldaddfinalizer方法在測試結(jié)束后通過調(diào)用它們的代碼來工作,但addfinalizeryield相比有兩個關(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í)行,沒有什么改變:

無smtpserver屬性的截圖

再讓我們快速創(chuàng)建另一個測試模塊,在其模塊名稱空間中實際設(shè)置服務(wù)器URL,新建一個test_anothersmtp.py文件,輸入以下代碼:

smtpserver = "mail.python.org"

def test_showhelo(smtp):
    assert 0, smtp.helo()

然后我們運行它:

有smtpserver屬性的截圖

大家可以看到,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í)行一次:

fixture參數(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:

使用ids關(guān)鍵字參數(shù)的截圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容