Chapter 5
pytest fixtures: explicit, modular, scalable
pytest fixtures: 明確、模塊化、可伸縮
翻譯水平低,只是方便自己以后查看
測試夾具Fixtures的目的,是為了讓測試可以可靠地重復(fù)執(zhí)行提供一個(gè)基準(zhǔn)。pytest fixtures 在傳統(tǒng)的xUnit風(fēng)格的 setup/teardown 函數(shù)基礎(chǔ)上提出了顯著的提高:
- fixtures 具有顯示的名字,并通過從函數(shù)、模塊、類或整改項(xiàng)目中聲明它們的使用來激活它們。
- fixtures 以模塊化的方式實(shí)現(xiàn),因?yàn)槊總€(gè)fixture名稱都觸發(fā)一個(gè)fixture函數(shù),該函數(shù)可以使用其他fixture。
- fixture 可用的范圍可以從簡單的單元測試到復(fù)雜的功能測試,允許根據(jù)配置和組件選項(xiàng)參數(shù)化fixtures和測試,或者跨函數(shù)、類、模塊或整個(gè)測試session范圍內(nèi)重用fixture。
此外,pytest 繼續(xù)支持傳統(tǒng)的xUnit風(fēng)格的setup,你可以混可兩種風(fēng)格,以增量的方式遷移到新的風(fēng)格,隨你喜歡。你也可以從已經(jīng)存在的unittest.TestCase風(fēng)格或nose based項(xiàng)目開始。
5.1 以函數(shù)參數(shù)形式的fixture
測試函數(shù)可以將fixture對象命名為輸入?yún)?shù)來接收他們。對于每一個(gè)參數(shù)名,具有該名稱的fixture函數(shù)提供fixture對象。fixture函數(shù)通過標(biāo)記@pytest.fixture來注冊。我們來看看一個(gè)簡單的自帶測試代碼的函數(shù)模塊,其中含有一個(gè)fixture和一個(gè)使用它的測試函數(shù):
# content of ./test_smtpsimple.py
import pytest
import smtplib
@pytest.fixture
def smtp_connection():
return smtplib.SMTP('smtp.qq.com', 587, timout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
這里test_ehlo需要smtp_connectionfixture的值。pytest將發(fā)現(xiàn)并調(diào)用被@pytest.fixture標(biāo)記的smtp_connectionfixtrue 函數(shù)。運(yùn)行這個(gè)測試看起來像這樣:
$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_smtpsimple.py F [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_smtpsimple.py:11: AssertionError
========================= 1 failed in 0.12 seconds =========================
在故障回溯中,我們看到測試函數(shù)被smtp_connection參數(shù)調(diào)用了,smtplib.SMTP()實(shí)例被fixture函數(shù)創(chuàng)建。測試函數(shù)失敗在故意設(shè)置的assert 0上。下面是pytest調(diào)用測試函數(shù)使用的具體協(xié)議:
- 因?yàn)?code>test_前綴,pytest 找到
test_ehlo。測試函數(shù)需要一個(gè)名為smtp_connection的函數(shù)參數(shù)。查找一個(gè)被fixtrue標(biāo)記的名為smtp_connection的函數(shù),發(fā)現(xiàn)了一個(gè)匹配的fixture函數(shù)。 -
smtp_connection()被調(diào)用來創(chuàng)建一個(gè)實(shí)例。 -
test_ehlo(<smtp_connection instance>)被調(diào)用并失敗在測試函數(shù)的最后一行
Note:如果你寫錯(cuò)了一個(gè)函數(shù)參數(shù)或者想使用的不可用,你將看到一個(gè)含有可用函數(shù)參數(shù)的列表的錯(cuò)誤
你通常可以執(zhí)行下列代碼查看可用的fixtures(以_開頭的fixtures只有在你加上-v選項(xiàng)的時(shí)候才會顯示)。
pytest --fixtures test_simplefactory.py
5.2 Fixtures: 一個(gè)依賴注入的典型例子
Fixtures 允許測試函數(shù)輕易地接收并處理特定的預(yù)初始化應(yīng)用程序?qū)ο?,不需要關(guān)心 import setup cleanup 的細(xì)節(jié)。這是依賴注入的一個(gè)典型例子,其中fixture函數(shù)充當(dāng)注入器的角色,而測試函數(shù)則是fixture對象的消費(fèi)者。
5.3 conftest.py共用fixture函數(shù)
如果在實(shí)現(xiàn)測試的過程中你意識到想要在多個(gè)測試文件中使用同一個(gè)fuxture函數(shù),你可以將其移動(dòng)到conftest.py文件中。你無需import這個(gè)fixture,pytest會自動(dòng)找到它。fixture函數(shù)的發(fā)現(xiàn)從測試類開始,然后是測試模塊,然后是conftest.py文件和內(nèi)置、第三方插件。
你也可以使用conftest.py文件實(shí)現(xiàn)每個(gè)目錄的本地插件。
5.4 共用測試數(shù)據(jù)
如果你想讓你的測試可以使用文件中的測試數(shù)據(jù),一個(gè)好方式是,通過加載這些數(shù)據(jù)到一個(gè)被測試使用的fixture里。這利用到了pytest的自動(dòng)緩存機(jī)制。
5.5 Scope:在類、模塊或會話中共用一個(gè)fixture實(shí)例
需要網(wǎng)絡(luò)訪問的fixtures依賴于連接性,通常創(chuàng)建這些非常耗時(shí)。擴(kuò)展一下前面的實(shí)例,我們可以向@pytest.fixture添加一個(gè)scope='module'參數(shù),以使每個(gè)測試模塊只調(diào)用一次被@pytest.fixture修飾的smtp_connectionfixture函數(shù)。在一個(gè)測試模塊里得多個(gè)測試函數(shù)將接受相同的smtp_connectionfixture實(shí)例,從而節(jié)省時(shí)間。scope參數(shù)接受的值可以是:function class module package or session. function 是缺省值。
下面的例子是將fixture函數(shù)放在了一個(gè)單獨(dú)的conftest.py文件中,所以在同一個(gè)路徑下的測試模塊都可以訪問這個(gè)fixture函數(shù):
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope='module'):
def smtp_connection():
return smtplib.SMTP('smtp.qq.com', 587, timeout=5)
這個(gè)fixture的名稱也是smtp_connection,你可以在任何conftest.py所在目錄或目錄下的測試或fixture中通過列出名稱smtp_connection作為入?yún)⒌男问絹碓L問它的結(jié)果。
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehclo()
assert response == 250
assert b'smtp.qq.com' in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
我們故意插入導(dǎo)致失敗的assert 0語句,以便檢查發(fā)生了什么,現(xiàn)在運(yùn)行這個(gè)測試:
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
E assert 0
test_module.py:6: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
========================= 2 failed in 0.12 seconds =========================
你可以看見兩個(gè)assert 0的失敗,但是更重要的是你可以看到同樣的(module-scoped)smtp_connection對象傳入了兩個(gè)測試函數(shù)里,pytest顯示傳入?yún)?shù)中的值。因此,由于使用了同一個(gè)smtp_connection實(shí)例,兩個(gè)測試函數(shù)運(yùn)行起來與一個(gè)測試函數(shù)一樣快。
如果你決定你更想要個(gè)作用域是會話的smtp_connection實(shí)例,你可以簡單地聲明它:
@pytest.fixture(scope='session')
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
最終,這個(gè)類的作用域的每個(gè)測試類將調(diào)用這個(gè)fixture一次。
Note: pytest一次只緩存一個(gè)fixture實(shí)例。這意味著當(dāng)使用一個(gè)參數(shù)化的fixture時(shí),pytest可以在指定的作用域內(nèi)多次調(diào)用一個(gè)fixture。
5.5.1 package作用域(實(shí)驗(yàn)中)
在pytest 3.7中,引入了package作用域。包作用域的fixture在一個(gè)包的測試都結(jié)束后完成。
警告:該功能還在實(shí)驗(yàn)中,如果在更多的使用中發(fā)現(xiàn)了嚴(yán)重的問題,可能在未來的版本中會被刪除。請謹(jǐn)慎使用此功能,并請務(wù)必向我們報(bào)告您發(fā)現(xiàn)的任何問題。
5.6 更高作用域的fixture會被首先實(shí)例化
在功能請求的特性中,更高作用域的fixture(例如session)比低作用域的fixture(例如function 或class)更先實(shí)例化。同一作用域的fixture的相對順序,依照在測試函數(shù)中的聲明順序和fixtures之間的依賴關(guān)系。
參看如下代碼:
@pytest.fixture(scope="session")
def s1():
pass
@pytest.fixture(scope="module")
def m1():
pass
@pytest.fixture
def f1(tmpdir):
pass
@pytest.fixture
def f2():
pass
def test_foo(f1, m1, f2, s1):
...
test_foo請求的fixtures依照以下順序?qū)嵗?/p>
- s1:是最高作用域的fixture(session)
- m1:是第二高作用域的fixture(module)
- tmpdir:是一個(gè)function作用域的fixture,被f1請求,需要被實(shí)例化,因?yàn)樗莊1的依賴項(xiàng)
- f1:是
test_foo參數(shù)列表中的第一個(gè)function作用域的fixture - f2:是
test_foo參數(shù)列表中的最后一個(gè)function作用域的fixture
5.7 fixture結(jié)束/執(zhí)行teardown代碼
pytest支持fixture運(yùn)行到作用域外的時(shí)候執(zhí)行特殊的結(jié)束代碼。通過使用一個(gè)yield聲明代替return,所有在yield聲明之后的代碼將會作為teardown代碼:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
不管測試的結(jié)果如何,print和smtp.close()聲明將在模塊的最后一個(gè)測試運(yùn)行結(jié)束后運(yùn)行。讓我們來運(yùn)行它:
$ pytest -s -q --tb==no
FFteardown smtp
2 failed in 0.12 seconds
我們看見smtp_connection實(shí)例在兩個(gè)測試運(yùn)行完成的時(shí)候結(jié)束了。
Note:如果我們用
scope='function'裝飾fixture函數(shù),fixture的setup和cleanup會在每一個(gè)測試執(zhí)行。測試模塊的任一個(gè)用例都不用修改或者知道fixture的setup。
Note:我們同樣可以將yield與with類似地使用:
# content of test_yield2.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.qq.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
smtp_connection鏈接將會在測試結(jié)束運(yùn)行后被關(guān)閉,因?yàn)?code>smtp_connection對象在with結(jié)束后自動(dòng)關(guān)閉。
Note:如果在
setup代碼期間(yield關(guān)鍵字之前的代碼)發(fā)生了異常,teardown代碼(yield關(guān)鍵字之后的代碼)將不會被調(diào)用。
一個(gè)可供替代的運(yùn)行teardown代碼選項(xiàng)是使用request-context對象的addfinalizer方法來注冊結(jié)束函數(shù)。
這里是smtp_commectionfixture修改為使用addfinalizer作為cleanup:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def fin():
print("teardown smtp_connection")
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection # provide the fixture value
yield和addfinalizer方法工作方式都類似,都是在測試結(jié)束后調(diào)用他們的代碼,但是addfinalzer相較于yield有兩個(gè)不同的點(diǎn):
- 可能有多個(gè)結(jié)束方法。
- 如果fixture
setup代碼發(fā)生了異常,結(jié)束代碼依然會被調(diào)用。即使setup代碼發(fā)生了再多的創(chuàng)建失敗/獲取失敗,也能適時(shí)地關(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將適時(shí)地關(guān)閉。當(dāng)然,如果在注冊結(jié)束函數(shù)前發(fā)生異常,它就不會運(yùn)行。
5.8 fixture 可以對請求它的測試內(nèi)容進(jìn)行自?。ǚ聪颢@取測試函數(shù)的環(huán)境)
fixture函數(shù)可以接受request對象,用來對“requesting”測試函數(shù)、類或者模塊內(nèi)容進(jìn)行自省。進(jìn)一步擴(kuò)展前面的smtp_connectionfixture例子,讓我們從一個(gè)使用我們的fixture的測試模塊讀取一個(gè)可選的服務(wù)器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, 587, timeout=5)
yield smtp_connection
print("finalizing %s (%s)" % (smtp_connection, server))
smtp_connection.close()
我們使用request.module屬性可選擇地從測試模塊獲取一個(gè)smtpserver屬性。如果我們只是再一次運(yùn)行,不會有什么變化:
$ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.qq.com)
2 failed in 0.12 seconds
讓我們快速地創(chuàng)建另一個(gè)測試模塊,實(shí)際地設(shè)置服務(wù)器URL到模塊的命名空間里:
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
assert 0, smtp_connection.helo()
運(yùn)行它:
$ pytest -qq --tb=short test_anothersmtp.py
F [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:5: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
瞧!這個(gè)smtp_connection fixture函數(shù)從模塊的命名空間里獲取了我們的郵箱服務(wù)器名字。
5.9 將fixture作為工廠
factory as fixture模式,可以在“單個(gè)測試中多次需要fixture”的情況下提供幫助。fixture沒有直接返回?cái)?shù)據(jù),而是返回一個(gè)生成數(shù)據(jù)的函數(shù)。這個(gè)函數(shù)可以被測試多次調(diào)用。
工廠可以有所需的參數(shù):
@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")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
如果工廠創(chuàng)建的數(shù)據(jù)需要管理,fixture可以處理:
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
5.10 參數(shù)化fixtures
fixture函數(shù)可以在被參數(shù)化,參數(shù)化后被多次調(diào)用,每次執(zhí)行依賴測試集合,相當(dāng)于這些測試依賴于這個(gè)fixture。測試函數(shù)通常不會需要知道他們的重新運(yùn)行。fuxture參數(shù)有助于為組件編寫詳細(xì)的功能測試,組件本身可以通過多種方式配置。
擴(kuò)展之前的例子,我們可以標(biāo)記fixture來創(chuàng)建兩個(gè)smtp_connectionfixture實(shí)例,這將導(dǎo)致所有的測試使用rixture運(yùn)行兩次。fixture函數(shù)通過特殊的request對象訪問每個(gè)參數(shù):
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print("finalizing %s" % smtp_connection)
smtp_connection.close()
主要的變化是帶有@pytest.fixture聲明的praram,它是一組值,fixture函數(shù)每次運(yùn)行會通過request.param訪問其中一個(gè)值的。不需要修改測試函數(shù)的代碼。那么我們跑一下:
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.qq.com" in msg
> assert 0 # for demo purposes
E assert 0
test_module.py:6: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert b"smtp.qq.com" in msg
E AssertionError: assert b'smtp.qq.com' in b'mail.python.
→org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-
→MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
test_module.py:5: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
________________________ test_noop[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_module.py:11: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
4 failed in 0.12 seconds
我們看見我們的兩個(gè)測試函數(shù)都跑了兩次,使用了不同的smtp_connection實(shí)例。
Note: 對于
mail.python.org,test_ehlo的第二次失敗是因?yàn)轭A(yù)期的服務(wù)器字符串與最終的不同。
pytest將構(gòu)建一個(gè)字符串,這個(gè)字符串是參數(shù)化fixture中的每個(gè)值得測試ID,比如在上面例子中的test_ehlo[smtp.gmail.com]和test_ehlo[mail.python.org] 。這些ID可以和-k一起使用,以旋轉(zhuǎn)要運(yùn)行的特定cases,當(dāng)一個(gè)case失敗時(shí),他們還將標(biāo)記特定的cases。使用--collect-only運(yùn)行pytest將顯示生成的ID。
Numbers, strings, booleans and None將在測試ID中顯示他們通常的字符串表示形式。對于其他的對象,pytest將做一個(gè)基于參數(shù)名字的字符串??梢允褂?code>ids關(guān)鍵字參數(shù)為某個(gè)fixture值定制測試ID中使用的字符串:
# content of 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
上面的內(nèi)容顯示ids可以是要使用的字符串列表,也可以是使用fixture值調(diào)用的函數(shù),然后必須返回要使用的字符串。在稍后的情況中,如果函數(shù)返回None,則使用pytest自動(dòng)生成的ID。
在下列被使用的測試測試ID運(yùn)行上面的測試,結(jié)果如下:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 10 items
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
<Function test_b[eggs]>
<Function test_b[1]>
<Module test_module.py>
<Function test_ehlo[smtp.gmail.com]>
<Function test_noop[smtp.gmail.com]>
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= no tests ran in 0.12 seconds =======================
5.11 使用參數(shù)化標(biāo)記
pytest.param()可以在參數(shù)化fixtrue的值集中應(yīng)用標(biāo)記,方法與@pytest.mark.parametrize相同。
比如:
# content of test_fixture_marks.py
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
運(yùn)行這個(gè)測試將會跳過值為2的data_set聲明:
$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_
→PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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 [100%]
=================== 2 passed, 1 skipped in 0.12 seconds ====================
5.12 模塊化:使用fixture函數(shù)中的fixtures
你不僅僅可以在測試函數(shù)中使用fixtures,還可以使用fixture函數(shù)可以使用其他的fixture。這有助于fixture的模塊化設(shè)計(jì),并允許跨許多項(xiàng)目重用特定框架的fixture。作為一個(gè)簡單的例子,我們可以擴(kuò)展之前的例子并實(shí)例化一個(gè)app對象,我們將已經(jīng)定義好的smtp_connection資源插入其中:
# content of test_appsetup.py
import pytest
class App(object):
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
這里,我們聲明了一個(gè)appfixture,它會接收之前定義的smtp_connectionfixture并為它實(shí)例化一個(gè)app對象。讓我們運(yùn)行一下:
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 2 items
test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
========================= 2 passed in 0.12 seconds =========================
由于smtp_connection的參數(shù)化,測試將在兩個(gè)不同的App實(shí)例和各自的smtp服務(wù)器上運(yùn)行兩次。appfixture不需要關(guān)注smtp_connection參數(shù)化,因?yàn)閜ytest將全面分析fixture依賴關(guān)系圖。
Note:
appfixture的作用域是模塊,且使用了一個(gè)模塊作用域的smtp_connectionfixture。如果smtp_connection的緩存在session作用域,這個(gè)示例仍然可以正常工作:fixture可以使用“更廣泛的”作用域fixture,但是反過來不行:session作用域的fixture不能以有意義的方式使用作用域?yàn)?code>module的fixture。
5.13 按照fixture實(shí)例自動(dòng)將測試分組
pytest 最小化了再測試運(yùn)行期間活動(dòng)fixture的數(shù)量。如果你有參數(shù)化的fixture,所有使用了它的測試將首先使用一個(gè)實(shí)例執(zhí)行,然后再下一個(gè)fixture實(shí)例創(chuàng)建之前調(diào)用終結(jié)器。除此之外,這還簡化了對創(chuàng)建和使用全局狀態(tài)的應(yīng)用程序的測試。
接下來的例子,使用了兩個(gè)參數(shù)化的fixtrue,一個(gè)作用域是每個(gè)模塊,所有的函數(shù)執(zhí)行print顯示setup/teardown流程:
# content of test_module.py
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP modarg %s" % param)
yield param
print(" TEARDOWN modarg %s" % param)
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print(" SETUP otherarg %s" % param)
yield param
print(" TEARDOWN otherarg %s" % param)
def test_0(otherarg):
print(" RUN test0 with otherarg %s" % otherarg)
def test_1(modarg):
print(" RUN test1 with modarg %s" % modarg)
def test_2(otherarg, modarg):
print(" RUN test2 with otherarg %s and modarg %s" % (otherarg, modarg))
讓我們運(yùn)行這些測試,使用詳細(xì)模式并關(guān)注打印輸出:
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_REFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod1-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2
RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod2-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2
========================= 8 passed in 0.12 seconds =========================
你可以見到參數(shù)化了的作用域是模塊的modarg資源影響了測試執(zhí)行的順序,從而導(dǎo)致了盡可能少的“活動(dòng)”資源。mod1的參數(shù)化資源的終結(jié)器將在mod2資源的setup之前執(zhí)行。
需要特別注意,test_0是完全獨(dú)立的且首先完成。然后使用mod1的test_1執(zhí)行,然后是使用mod1的test_2執(zhí)行,然后使用mod2的test_1執(zhí)行,最后是使用mod2的test_2執(zhí)行。
otherarg參數(shù)化資源(擁有函數(shù)作用域)在每一個(gè)使用它的測試之前set up并在其之后tear down。
5.14 從class、module或者project中引用fixture
有時(shí),測試函數(shù)不需要直接訪問fixture對象。例如,測試可能會操作一個(gè)空的目錄作為當(dāng)前工作目錄進(jìn)行操作,同時(shí)又不關(guān)心具體是什么目錄。下面是如何使用標(biāo)準(zhǔn)的tempfile和pytest fixture來實(shí)現(xiàn)它。我們將fixture的創(chuàng)建分離到conftest.py文件中:
# content of conftest.py
import pytest
import tempfile
import os
@pytest.fixture()
def cleandir():
newpath = tempfile.mkdtemp()
os.chdir(newpath)
并且通過usefixture標(biāo)記在測試模塊中聲明它的使用:
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit(object):
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
由于usefixtures標(biāo)記,每個(gè)測試方法的運(yùn)行都需要請求cleanfirfixture,就好像你為他們每個(gè)指定了一個(gè)cleandir函數(shù)參數(shù)一樣。讓我們運(yùn)行一下來嚴(yán)重我們的fixture是激活并測試通過了:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
你可以像這樣指定多個(gè)fixture:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
你可以使用mark機(jī)制的通用特性,在測試模塊級別指定fixture的使用方式:
pytestmark = pytest.mark.usefixture('cleandir')
Note: 指定的變量必須名為
pytestmark,例如foomark將不會激活fixture。也可以將項(xiàng)目中所有測試所需的fixture放入一個(gè)ini文件中:
# content of pytest.ini
[pytest]
usefixtures = cleandir
Warning:注意這里的標(biāo)記對fixture函數(shù)沒有效果。例如,這將不會像預(yù)期的那樣工作:
@pytest.mark.usefixtures("my_other_fixture") @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture(): ...目前,這不會生產(chǎn)任何錯(cuò)誤或警告,但是這將由#3664處理。
5.15 自動(dòng)調(diào)用fixture(xUnit setup on steroids)
有時(shí)候,你可能想在不顯示聲明函數(shù)參數(shù)或usefixtures裝飾器的情況下自動(dòng)調(diào)用fixture。作為一個(gè)世紀(jì)的例子,假設(shè)我們有一個(gè)數(shù)據(jù)庫fixture,它具有一個(gè)begin/rollback/commit體系結(jié)構(gòu),并且我們希望通過事務(wù)和回滾自動(dòng)地包裹每個(gè)測試方法。這里是這個(gè)想法的虛擬的自包含實(shí)現(xiàn):
# content of test_db_transact.py
import pytest
class DB(object):
def __init__(self):
self.intransaction = []
def begin(self, name):
self.intransaction.append(name)
def rollback(self):
self.intransaction.pop()
@pytest.fixture(scope="module")
def db():
return DB()
class TestClass(object):
@pytest.fixture(autouse=True)
def transact(self, request, db):
db.begin(request.function.__name__)
yield
db.rollback()
def test_method1(self, db):
assert db.intransaction == ["test_method1"]
def test_method2(self, db):
assert db.intransaction == ["test_method2"]
class級別的transactfixture被標(biāo)記了autouse=true,意味著類中的所有測試方法都將使用這個(gè)fixture,二不需要在測試函數(shù)簽名中聲明它,也不需要使用類級別的usefixture裝飾器。
如果我們運(yùn)行它,會得到兩個(gè)通過的測試:
$ pytest -q
.. [100%]
2 passed in 0.12 seconds
這里是在其他作用域 autouse的fixture怎么工作
- autouse fixture遵循
scope=參數(shù):如果一個(gè)autouse fixture具有scope='session',那么他講值運(yùn)行一次,無論它在哪里定義的。scope='class'表示它將在每個(gè)類中運(yùn)行一次,等等。 - 如果在測試模塊中定義了一個(gè)autouse fixture,那么這個(gè)模塊下的所有測試函數(shù)都會自動(dòng)使用它。
- 如果在
conftest.py文件中定義了一個(gè)autouse fuxture,那么在同一個(gè)目錄下所有測試模塊中的所有測試都會調(diào)用該fixture - 最后,使用的時(shí)候請小心:如果在插件中定義了一個(gè)autouse fixture,那么它將被所有安裝了插件的項(xiàng)目中的所有測試使用。如果fixture只是在某些(例如在ini文件中)設(shè)置存在的情況下工作,那么它將非常有用。這樣的全局fixture應(yīng)該總是快速遞確定它是否應(yīng)該執(zhí)行任何工作,避免了其他昂貴的導(dǎo)入或計(jì)算。
請注意,上面的transactionfixture很可能是希望你在項(xiàng)目中使用的fixture,而不需要它處于激活狀態(tài)。規(guī)范的方法是將這個(gè)事務(wù)定義在一個(gè)conftest.py文件中,而不是使用autouse:
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
并且例如,有一個(gè)TestClass使用它來聲明需要使用這個(gè)fixture:
@pytest.mark.usefixtures("transact")
class TestClass(object):
def test_method1(self):
...
所有在TestClass里的測試方法將會使用transactionfixture,而模塊中的其他測試類或函數(shù)將不會使用它,除非它們也添加了transact引用。
5.16 覆蓋不同級別的fixture
在相對較大的測試套件中,你極大可能需要用本地定義的fixture覆蓋全局或根fixture,以保持測試代碼的可讀性和可維護(hù)性。
5.16.1覆蓋文件夾(conftest.py)級別的fixture
給定的測試文件結(jié)構(gòu)為:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# content of tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
如你所見,一個(gè)相同名字的fixture可以被子文件夾中的fixture覆蓋。請注意,在上面的例子中,可以從overridingfixture輕易地訪問base或者superfixture。
5.16.2 在測試模塊級別覆蓋fixture
給定的測試文件結(jié)構(gòu)如下:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
在上面的例子中,具有相同名字的fixture可以被某些測試模塊覆蓋。
5.16.3 直接用測試的參數(shù)化覆蓋fixture
給定的測試文件結(jié)構(gòu)如下:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
在上面的例子中,fixture的值被測試參數(shù)的值覆蓋了。要注意的是即使沒用直接使用fixture的值(在函數(shù)原型中沒有提到),也可以用這種方式覆蓋。
5.16.4 用非參數(shù)化的fixture覆蓋參數(shù)化的fixture,反過來也可以
給定的測試文件結(jié)構(gòu)如下:
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
在上面的例子中,參數(shù)化的fixture被非參數(shù)化版本覆蓋,而非參數(shù)化fixture被某些測試模塊的參數(shù)化版本覆蓋。顯然,測試文件夾級別也是如此。
【第5章 完】