目錄:
- 安裝及入門
- 使用和調(diào)用方法
- 原有TestSuite使用方法
- 斷言的編寫和報告
- Pytest fixtures:清晰 模塊化 易擴展
- 使用Marks標記測試用例
- Monkeypatching/對模塊和環(huán)境進行Mock
- 使用tmp目錄和文件
- 捕獲stdout及stderr輸出
- 捕獲警告信息
- 模塊及測試文件中集成doctest測試
- skip及xfail: 處理不能成功的測試用例
- Fixture方法及測試用例的參數(shù)化
- 緩存: 使用跨執(zhí)行狀態(tài)
- unittest.TestCase支持
- 運行Nose用例
- 經(jīng)典xUnit風格的setup/teardown
- 安裝和使用插件
- 插件編寫
- 編寫鉤子(hook)方法
- 運行日志
- API參考
- 優(yōu)質(zhì)集成實踐
- 片狀測試
- Pytest導入機制及sys.path/PYTHONPATH
- 配置選項
- 示例及自定義技巧
- Bash自動補全設(shè)置
Pytest fixtures:清晰 模塊化 易擴展
2.0/2.3/2.4版本新功能
text fixtures的目的是為測試的重復執(zhí)行提供一個可靠的固定基線。 pytest fixture比經(jīng)典的xUnit setUp/tearDown方法有著顯著的改進:
- fixtures具有明確的名稱,在測試方法/類/模塊或整個項目中通過聲明使用的fixtures名稱來使用。
- fixtures以模塊化方式實現(xiàn),因為每個fixture名稱都會觸發(fā)調(diào)用fixture函數(shù),該fixture函數(shù)本身可以使用其它的fixtures。
- 從簡單的單元測試到復雜的功能測試,fixtures的管理允許根據(jù)配置和組件選項對fixtures和測試用例進行參數(shù)化,或者在測試方法/類/模塊或整個測試會話范圍內(nèi)重復使用該fixture。
此外,pytest繼續(xù)支持經(jīng)典的xUnit風格的setup方法。 你可以根據(jù)需要混合使用兩種樣式,逐步從經(jīng)典樣式移動到新樣式。 您也可以從現(xiàn)有的unittest.TestCase樣式或基于nose的項目開始。
Fixtures作為函數(shù)參數(shù)使用
測試方法可以通過在其參數(shù)中使用fixtures名稱來接收fixture對象。 每個fixture參數(shù)名稱所對應的函數(shù),可以通過使用@pytest.fixture
注冊成為一個fixture函數(shù),來為測試方法提供一個fixture對象。 讓我們看一個只包含一個fixture和一個使用它的測試方法的簡單獨立測試模塊:
# ./test_smtpsimple.py內(nèi)容
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
這里,test_ehlo
需要smtp_connection
來提供fixture對象。pytest將發(fā)現(xiàn)并調(diào)用帶@pytest.fixture
裝飾器的smtp_connection
fixture函數(shù)。 運行測試如下所示:
$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
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 =========================
在測試失敗的回溯信息中,我們看到測試方法是使用smtp_connection
參數(shù)調(diào)用的,即由fixture函數(shù)創(chuàng)建的smtplib.SMTP()
實例。測試用例在我們故意的assert 0
上失敗。以下是pytest用這種方式調(diào)用測試方法使用的確切協(xié)議:
Fixtures: 依賴注入的主要例子
Fixtures允許測試方法能輕松引入預先定義好的初始化準備函數(shù),而無需關(guān)心導入/設(shè)置/清理方法的細節(jié)。 這是依賴注入的一個主要示例,其中fixture函數(shù)的功能扮演”注入器“的角色,測試方法來“消費”這些fixture對象。
conftest.py: 共享fixture函數(shù)
如果在測試中需要使用多個測試文件中的fixture函數(shù),則可以將其移動到conftest.py
文件中,所需的fixture對象會自動被pytest
發(fā)現(xiàn),而不需要再每次導入。 fixture函數(shù)的發(fā)現(xiàn)順序從測試類開始,然后是測試模塊,然后是conftest.py文件,最后是內(nèi)置和第三方插件。
你還可以使用conftest.py文件來實現(xiàn)本地每個目錄的插件。
共享測試數(shù)據(jù)
如果要使用數(shù)據(jù)文件中的測試數(shù)據(jù),最好的方法是將這些數(shù)據(jù)加載到fixture函數(shù)中以供測試方法注入使用。這利用到了pytest
的自動緩存機制。
另一個好方法是在tests文件夾中添加數(shù)據(jù)文件。 還有社區(qū)插件可用于幫助處理這方面的測試,例如:pytest-datadir
和pytest-datafiles
。
生效范圍:在測試類/測試模塊/測試會話中共享fixture對象
由于fixtures對象需要連接形成依賴網(wǎng),而通常創(chuàng)建時間比較長。 擴展前面的示例,我們可以在@pytest.fixture
調(diào)用中添加scope ="module"
參數(shù),以使每個測試模塊只調(diào)用一次修飾的smtp_connection
fixture函數(shù)(默認情況下,每個測試函數(shù)調(diào)用一次)。 因此,測試模塊中的多個測試方法將各自注入相同的smtp_connection
fixture對象,從而節(jié)省時間。scope參數(shù)的可選值包括:function(函數(shù)), class(類), module(模塊), package(包)及 session(會話)。
下一個示例將fixture函數(shù)放入單獨的conftest.py文件中,以便來自目錄中多個測試模塊的測試可以訪問fixture函數(shù):
# conftest.py文件內(nèi)容
import pytest
import smtplib
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
fixture對象的名稱依然是smtp_connection
,你可以通過在任何測試方法或fixture函數(shù)(在conftest.py所在的目錄中或下面)使用參數(shù)smtp_connection
作為輸入?yún)?shù)來訪問其結(jié)果:
# test_module.py文件內(nèi)容
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
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
我們故意插入失敗的assert 0
語句,以便檢查發(fā)生了什么,運行測試并查看結(jié)果:
$ 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, inifile:
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 =========================
你會看到兩個assert 0
失敗信息,更重要的是你還可以看到相同的(模塊范圍的)smtp_connection
對象被傳遞到兩個測試方法中,因為pytest在回溯信息中顯示傳入的參數(shù)值。 因此,使用smtp_connection
的兩個測試方法運行速度與單個函數(shù)一樣快,因為它們重用了相同的fixture對象。
如果您決定要使用session(會話,一次運行算一次會話)范圍的smtp_connection
對象,則只需如下聲明:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests needing it
...
最后,class(類)范圍將為每個測試類調(diào)用一次fixture對象。
注意:
Pytest一次只會緩存一個fixture實例。 這意味著當使用參數(shù)化fixture時,pytest可能會在給定范圍內(nèi)多次調(diào)用fixture函數(shù)。
package(包)范圍的fixture(實驗性功能)
3.7版本新功能
在pytest 3.7中,引入了包范圍。 當包的最后一次測試結(jié)束時,最終確定包范圍的fixture函數(shù)。
警告:
此功能是實驗性的,如果在獲得更多使用后發(fā)現(xiàn)隱藏的角落情況或此功能的嚴重問題,可能會在將來的版本中刪除。
謹慎使用此新功能,請務(wù)必報告您發(fā)現(xiàn)的任何問題。
高范圍的fixture函數(shù)優(yōu)先實例化
3.5版本新功能
在測試函數(shù)的fixture對象請求中,較高范圍的fixture(例如session會話級)較低范圍的fixture(例如function函數(shù)級或class類級優(yōu)先執(zhí)行。相同范圍的fixture對象的按引入的順序及fixtures之間的依賴關(guān)系按順序調(diào)用。
請考慮以下代碼:
@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將按以下順序執(zhí)行:
- s1:是最高范圍的fixture(會話級)
- m1:是第二高的fixture(模塊級)
- tmpdir:是一個函數(shù)級的fixture,f1依賴它,因此它需要在f1前調(diào)用
- f1:是test_foo參數(shù)列表中第一個函數(shù)范圍的fixture。
- f2:是test_foo參數(shù)列表中最后一個函數(shù)范圍的fixture。
fixture結(jié)束/執(zhí)行teardown代碼
當fixture超出范圍時,通過使用yield語句而不是return,pytest支持fixture執(zhí)行特定的teardown代碼。yield語句之后的所有代碼都視為teardown代碼:
# conftest.py文件內(nèi)容
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()
無論測試的異常狀態(tài)如何,print
和smtp.close()
語句將在模塊中的最后一個測試完成執(zhí)行時執(zhí)行。
讓我們執(zhí)行一下(上文的test_module.py
):
$ pytest -s -q --tb=no
FFteardown smtp
2 failed in 0.12 seconds
我們看到smtp_connection
實例在兩個測試完成執(zhí)行后完成。 請注意,如果我們使用scope ='function'
修飾我們的fixture函數(shù),那么每次單個測試都會進行fixture的setup和teardown。 在任何一種情況下,測試模塊本身都不需要改變或了解fixture函數(shù)的這些細節(jié)。
請注意,我們還可以使用with語句無縫地使用yield語法:
# test_yield2.py文件內(nèi)容
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
yield smtp_connection # provide the fixture value
測試結(jié)束后, smtp_connection
連接將關(guān)閉,因為當with語句結(jié)束時,smtp_connection
對象會自動關(guān)閉。
請注意,如果在設(shè)置代碼期間(yield關(guān)鍵字之前)發(fā)生異常,則不會調(diào)用teardown代碼(在yield之后)。
執(zhí)行teardown代碼的另一種選擇是利用請求上下文對象的addfinalizer
方法來注冊teardown函數(shù)。
以下是smtp_connection
fixture函數(shù)更改為使用addfinalizer
進行teardown:
# 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)用它們的代碼時的工作方式類似,但addfinalizer
相比yield
有兩個主要區(qū)別:
- 使用
addfinalizer
可以注冊多個teardown功能。 - 無論fixture中setup代碼是否引發(fā)異常,都將始終調(diào)用teardown代碼。 即使其中一個資源無法創(chuàng)建/獲取,也可以正確關(guān)閉fixture函數(shù)創(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”因異常而失敗,則“C1”和“C3”仍將正確關(guān)閉。 當然,如果在注冊finalize
函數(shù)之前發(fā)生異常,那么它將不會被執(zhí)行。
Fixtures中使用測試上下文的內(nèi)省信息
Fixtures工廠方法
Fixtures參數(shù)化
使用參數(shù)化fixtures標記
模塊化:在fixture函數(shù)中使用fixtures功能
使用fixture實例自動組織測試用例
在類/模塊/項目中使用fixtures
自動使用fixtures(xUnit 框架的setup固定方法)
不同級別的fixtures的覆蓋(優(yōu)先級)
相對于在較大范圍的測試套件中的Test Fixtures方法,在較小范圍子套件你可能需要重寫和覆蓋外層的Test Fixtures方法,從而保持測試代碼的可讀性和可維護性。
在文件夾級別(通過conftest文件)重寫fixtures方法
假設(shè)用例目錄結(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'
你可以看到, 基礎(chǔ)/上級fixtures方法可以通過子文件夾下的con
ftest.py中同名的fixtures方法覆蓋, 非常簡單, 只需要按照上面的例子使用即可.
在測試模塊級別重寫fixtures方法
假設(shè)用例文件結(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方法會覆蓋文件夾conftest.py中同名的fixtures方法
在直接參數(shù)化方法中覆蓋fixtures方法
假設(shè)用例文件結(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'
在上面的示例中,username fixture方法的結(jié)果值被參數(shù)化值覆蓋。 請注意,即使測試不直接使用(也未在函數(shù)原型中提及),也可以通過這種方式覆蓋fixture的值。
使用非參數(shù)化fixture方法覆蓋參數(shù)化fixtures方法, 反之亦然
假設(shè)用例結(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ù)化fixture方法,以及使用參數(shù)化fixture覆蓋非參數(shù)化fixture以用于特定測試模塊。 這同樣適用于文件夾級別的fixtures方法。