目錄html
fixture
:做爲形參使用fixture
:一個典型的依賴注入的實踐conftest.py
:共享fixture
實例fixture
實例
fixture
的實例化順序fixture
的清理操做
fixture
能夠訪問測試請求的上下文fixture
返回工廠函數fixture
的參數化fixture
中標記用例fixture
使用其它的fixture
fixture
實例fixture
實例fixture
fixture
pytest fixtures
的目的是提供一個固定的基線,使測試能夠在此基礎上可靠地、重複地執行;對比xUnit
經典的setup/teardown
形式,它在如下方面有了明顯的改進:python
fixture
擁有一個明確的名稱,經過聲明使其可以在函數、類、模塊,甚至整個測試會話中被激活使用;fixture
以一種模塊化的方式實現。由於每個fixture
的名字都能觸發一個fixture函數,而這個函數自己又能調用其它的fixture
;fixture
的管理從簡單的單元測試擴展到複雜的功能測試,容許經過配置和組件選項參數化fixture
和測試用例,或者跨功能、類、模塊,甚至整個測試會話複用fixture
;此外,pytest
繼續支持經典的xUnit
風格的測試。你能夠根據本身的喜愛,混合使用兩種風格,或者逐漸過渡到新的風格。你也能夠從已有的unittest.TestCase
或者nose
項目中執行測試;git
fixture
:做爲形參使用測試用例能夠接收fixture
的名字做爲入參,其實參是對應的fixture
函數的返回值。經過@pytest.fixture
裝飾器能夠註冊一個fixture
;github
咱們來看一個簡單的測試模塊,它包含一個fixture
和一個使用它的測試用例:數據庫
# src/chapter-4/test_smtpsimple.py import pytest @pytest.fixture def smtp_connection(): import smtplib return smtplib.SMTP("smtp.163.com", 25, timeout=5) def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 assert 0 # 爲了展現,強制置爲失敗
這裏,test_ehlo
有一個形參smtp_connection
,和上面定義的fixture
函數同名;緩存
執行:bash
$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py F [100%] =============================== FAILURES ================================ _______________________________ test_ehlo _______________________________ smtp_connection = <smtplib.SMTP object at 0x105992d68> def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 > assert 0 # 爲了展現,強制置爲失敗 E assert 0 src/chapter-4/test_smtpsimple.py:35: AssertionError 1 failed in 0.17s
執行的過程以下:服務器
pytest
收集到測試用例test_ehlo
,其有一個形參smtp_connection
,pytest
查找到一個同名的已經註冊的fixture
;smtp_connection()
建立一個smtp_connection
實例<smtplib.SMTP object at 0x105992d68>
做爲test_ehlo
的實參;test_ehlo(<smtplib.SMTP object at 0x105992d68>)
;若是你不當心拼寫出錯,或者調用了一個未註冊的fixture
,你會獲得一個fixture <...> not found
的錯誤,並告訴你目前全部可用的fixture
,以下:網絡
$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py E [100%] ================================ ERRORS ================================= ______________________ ERROR at setup of test_ehlo ______________________ file /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py, line 32 def test_ehlo(smtp_connectio): E fixture 'smtp_connectio' not found > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, smtp_connection, smtp_connection_package, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py:32 1 error in 0.02s
注意:session
你也可使用以下調用方式:
pytest --fixtures [testpath]它會幫助你顯示全部可用的 fixture;
可是,對於
_
開頭的fixture
,須要加上-v
選項;
fixture
:一個典型的依賴注入的實踐fixture
容許測試用例能夠輕鬆的接收和處理特定的須要預初始化操做的應用對象,而不用過度關心導入/設置/清理的細節;這是一個典型的依賴注入的實踐,其中,fixture
扮演者注入者(injector
)的角色,而測試用例扮演者消費者(client
)的角色;
以上一章的例子來講明:test_ehlo
測試用例須要一個smtp_connection
的鏈接對象來作測試,它只關心這個鏈接是否有效和可達,並不關心它的建立過程。smtp_connection
對test_ehlo
來講,就是一個須要預初始化操做的應用對象,而這個預處理操做是在fixture中完成的;簡而言之,test_ehlo
說:「我須要一個SMTP
鏈接對象。」,而後,pytest
就給了它一個,就這麼簡單。
關於依賴注入的解釋,能夠看看Stackflow上這個問題的高票回答如何向一個5歲的孩子解釋依賴注入?:
When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.
What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.
更詳細的資料能夠看看維基百科Dependency injection;
conftest.py
:共享fixture
實例若是你想在多個測試模塊中共享同一個fixture
實例,那麼你能夠把這個fixture
移動到conftest.py
文件中。在測試模塊中你不須要手動的導入它,pytest
會自動發現,fixture
的查找的順序是:測試類、測試模塊、conftest.py
、最後是內置和第三方的插件;
你還能夠利用conftest.py
文件的這個特性爲每一個目錄實現一個本地化的插件;
若是你想多個測試共享一樣的測試數據文件,咱們有兩個好方法實現這個:
fixture
中,測試中再使用這些fixture
;tests
文件夾中,一些第三方的插件能幫助你管理這方面的測試,例如:pytest-datadir和pytest-datafiles;fixture
實例須要使用到網絡接入的fixture
每每依賴於網絡的連通性,而且建立過程通常都很是耗時;
咱們來擴展一下上述示例(src/chapter-4/test_smtpsimple.py):在@pytest.fixture
裝飾器中添加scope='module'
參數,使每一個測試模塊只調用一次smtp_connection
(默認每一個用例都會調用一次),這樣模塊中的全部測試用例將會共享同一個fixture
實例;其中,scope
參數可能的值都有:function
(默認值)、class
、module
、package
和session
;
首先,咱們把smtp_connection()
提取到conftest.py
文件中:
# src/chapter-4/conftest.py import pytest import smtplib @pytest.fixture(scope='module') def smtp_connection(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
而後,在相同的目錄下,新建一個測試模塊test_module.py
,將smtp_connection
做爲形參傳入每一個測試用例,它們共享同一個smtp_connection()
的返回值:
# src/chapter-4/test_module.py def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 smtp_connection.extra_attr = 'test' assert 0 # 爲了展現,強制置爲失敗 def test_noop(smtp_connection): response, _ = smtp_connection.noop() assert response == 250 assert smtp_connection.extra_attr == 0 # 爲了展現,強制置爲失敗
最後,讓咱們來執行這個測試模塊:
pipenv run pytest -q src/chapter-4/test_module.py FF [100%] =============================== FAILURES ================================ _______________________________ test_ehlo _______________________________ smtp_connection = <smtplib.SMTP object at 0x107193c50> def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 smtp_connection.extra_attr = 'test' > assert 0 # 爲了展現,強制置爲失敗 E assert 0 src/chapter-4/test_module.py:27: AssertionError _______________________________ test_noop _______________________________ smtp_connection = <smtplib.SMTP object at 0x107193c50> def test_noop(smtp_connection): response, _ = smtp_connection.noop() assert response == 250 > assert smtp_connection.extra_attr == 0 E AssertionError: assert 'test' == 0 E + where 'test' = <smtplib.SMTP object at 0x107193c50>.extra_attr src/chapter-4/test_module.py:33: AssertionError 2 failed in 0.72s
能夠看到:
smtp_connection
實例都是<smtplib.SMTP object at 0x107193c50>
,說明smtp_connection
只被調用了一次;test_ehlo
中修改smtp_connection
實例(上述例子中,爲smtp_connection
添加extra_attr
屬性),也會反映到test_noop
用例中;若是你指望擁有一個會話級別做用域的fixture
,能夠簡單的將其聲明爲:
@pytest.fixture(scope='session') def smtp_connection(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
注意:
pytest
每次只緩存一個fixture
實例,當使用參數化的fixture
時,pytest
可能會在聲明的做用域內屢次調用這個fixture
;
package
做用域(實驗性的)在 pytest 3.7 的版本中,正式引入了package
做用域。
package
做用域的fixture
會做用於包內的每個測試用例:
首先,咱們在src/chapter-4
目錄下建立以下的組織:
chapter-4/ └── package_expr ├── __init__.py ├── test_module1.py └── test_module2.py
而後,在src/chapter-4/conftest.py
中聲明一個package
做用域的fixture
:
@pytest.fixture(scope='package') def smtp_connection_package(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
接着,在src/chapter-4/package_expr/test_module1.py
中添加以下測試用例:
def test_ehlo_in_module1(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 assert 0 # 爲了展現,強制置爲失敗 def test_noop_in_module1(smtp_connection_package): response, _ = smtp_connection_package.noop() assert response == 250 assert 0 # 爲了展現,強制置爲失敗
一樣,在src/chapter-4/package_expr/test_module2.py
中添加以下測試用例:
def test_ehlo_in_module2(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 assert 0 # 爲了展現,強制置爲失敗
最後,執行src/chapter-4/package_expr
下全部的測試用例:
$ pipenv run pytest -q src/chapter-4/package_expr/ FFF [100%] =============================== FAILURES ================================ _________________________ test_ehlo_in_module1 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_ehlo_in_module1(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 > assert 0 # 爲了展現,強制置爲失敗 E assert 0 src/chapter-4/package_expr/test_module1.py:26: AssertionError _________________________ test_noop_in_module1 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_noop_in_module1(smtp_connection_package): response, _ = smtp_connection_package.noop() assert response == 250 > assert 0 E assert 0 src/chapter-4/package_expr/test_module1.py:32: AssertionError _________________________ test_ehlo_in_module2 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_ehlo_in_module2(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 > assert 0 # 爲了展現,強制置爲失敗 E assert 0 src/chapter-4/package_expr/test_module2.py:26: AssertionError 3 failed in 0.45s
能夠看到:
fixture
實例,即<smtplib.SMTP object at 0x1028fec50>
;注意:
chapter-4/package_expr
能夠不包含__init__.py
文件,由於pytest
發現測試用例的規則沒有強制這一點;一樣,package_expr/
的命名也不須要符合test_*
或者*_test
的規則;這個功能標記爲實驗性的,若是在其實際應用中發現嚴重的
bug
,那麼這個功能極可能被移除;
fixture
的實例化順序多個fixture
的實例化順序,遵循如下原則:
session
)先於低級別的做用域的(例如:class
或者function
)實例化;fixture
之間的相互調用關係;autouse
的fixture
,先於其同級別的其它fixture
實例化;咱們來看一個具體的例子:
# src/chapter-4/test_order.py import pytest order = [] @pytest.fixture(scope="session") def s1(): order.append("s1") @pytest.fixture(scope="module") def m1(): order.append("m1") @pytest.fixture def f1(f3): order.append("f1") @pytest.fixture def f3(): order.append("f3") @pytest.fixture(autouse=True) def a1(): order.append("a1") @pytest.fixture def f2(): order.append("f2") def test_order(f1, m1, f2, s1): assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
s1
擁有最高級的做用域(session
),即便在測試用例test_order
中最後被聲明,它也是第一個被實例化的(參照第一條原則)m1
擁有僅次於session
級別的做用域(module
),因此它是第二個被實例化的(參照第一條原則)f1 f2 f3 a1
同屬於function
級別的做用域:
test_order(f1, m1, f2, s1)
形參的聲明順序中,能夠看出,f1
比f2
先實例化(參照第二條原則)f1
的定義中又顯式的調用了f3
,因此f3
比f1
先實例化(參照第二條原則)a1
的定義中使能了autouse
標記,因此它會在同級別的fixture
以前實例化,這裏也就是在f3 f1 f2
以前實例化(參照第三條原則)因此這個例子fixture
實例化的順序爲:s1 m1 a1 f3 f1 f2
注意:
除了
autouse
的fixture
,須要測試用例顯示聲明(形參),不聲明的不會被實例化;多個相同做用域的
autouse fixture
,其實例化順序遵循fixture
函數名的排序;
fixture
的清理操做咱們指望在fixture
退出做用域以前,執行某些清理性操做(例如,關閉服務器的鏈接等);
咱們有如下幾種形式,實現這個功能:
yield
代替return
將fixture
函數中的return
關鍵字替換成yield
,則yield
以後的代碼,就是咱們要的清理操做;
咱們來聲明一個包含清理操做的smtp_connection
:
# src/chapter-4/conftest.py @pytest.fixture() def smtp_connection_yield(): smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5) yield smtp_connection print("關閉SMTP鏈接") smtp_connection.close()
再添加一個使用它的測試用例:
# src/chapter-4/test_smtpsimple.py def test_ehlo_yield(smtp_connection_yield): response, _ = smtp_connection_yield.ehlo() assert response == 250 assert 0 # 爲了展現,強制置爲失敗
如今,咱們來執行它:
λ pipenv run pytest -q -s --tb=no src/chapter-4/test_smtpsimple.py::test_ehlo_yield F關閉SMTP鏈接 1 failed in 0.18s
咱們能夠看到在test_ehlo_yield
執行完後,又執行了yield
後面的代碼;
with
寫法對於支持with
寫法的對象,咱們也能夠隱式的執行它的清理操做;
例如,上面的smtp_connection_yield
也能夠這樣寫:
@pytest.fixture() def smtp_connection_yield(): with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection: yield smtp_connection
addfinalizer
方法fixture
函數可以接收一個request
的參數,表示測試請求的上下文;咱們可使用request.addfinalizer
方法爲fixture
添加清理函數;
例如,上面的smtp_connection_yield
也能夠這樣寫:
@pytest.fixture() def smtp_connection_fin(request): smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5) def fin(): smtp_connection.close() request.addfinalizer(fin) return smtp_connection
注意:
在
yield
以前或者addfinalizer
註冊以前代碼發生錯誤退出的,都不會再執行後續的清理操做
fixture
能夠訪問測試請求的上下文fixture
函數能夠接收一個request
的參數,表示測試用例、類、模塊,甚至測試會話的上下文環境;
咱們能夠擴展上面的smtp_connection_yield
,讓其根據不一樣的測試模塊使用不一樣的服務器:
# src/chapter-4/conftest.py @pytest.fixture(scope='module') def smtp_connection_request(request): server, port = getattr(request.module, 'smtp_server', ("smtp.163.com", 25)) with smtplib.SMTP(server, port, timeout=5) as smtp_connection: yield smtp_connection print("斷開 %s:%d" % (server, port))
在測試模塊中指定smtp_server
:
# src/chapter-4/test_request.py smtp_server = ("mail.python.org", 587) def test_163(smtp_connection_request): response, _ = smtp_connection_request.ehlo() assert response == 250
咱們來看看效果:
λ pipenv run pytest -q -s src/chapter-4/test_request.py .斷開 mail.python.org:587 1 passed in 4.03s
fixture
返回工廠函數若是你須要在一個測試用例中,屢次使用同一個fixture
實例,相對於直接返回數據,更好的方法是返回一個產生數據的工廠函數;
而且,對於工廠函數產生的數據,也能夠在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")
fixture
的參數化若是你須要在一系列的測試用例的執行中,每輪執行都使用同一個fixture
,可是有不一樣的依賴場景,那麼能夠考慮對fixture
進行參數化;這種方式適用於對多場景的功能模塊進行詳盡的測試;
在以前的章節fixture能夠訪問測試請求的上下文中,咱們在測試模塊中指定不一樣smtp_server
,獲得不一樣的smtp_connection
實例;
如今,咱們能夠經過指定params
關鍵字參數建立兩個fixture
實例,每一個實例供一輪測試使用,全部的測試用例執行兩遍;在fixture
的聲明函數中,可使用request.param
獲取當前使用的入參;
# src/chapter-4/test_request.py @pytest.fixture(scope='module', params=['smtp.163.com', "mail.python.org"]) def smtp_connection_params(request): server = request.param with smtplib.SMTP(server, 587, timeout=5) as smtp_connection: yield smtp_connection
在測試用例中使用這個fixture
:
# src/chapter-4/test_params.py def test_parames(smtp_connection_params): response, _ = smtp_connection_params.ehlo() assert response == 250
執行:
$ pipenv run pytest -q -s src/chapter-4/test_params.py .斷開 smtp.163.com:25 .斷開 smtp.126.com:25 2 passed in 0.26s
能夠看到:
SMTP
服務器,執行了兩次;在參數化的fixture
中,pytest
爲每一個fixture
實例自動指定一個測試ID
,例如:上述示例中的test_parames[smtp.163.com]
和test_parames[smtp.126.com]
;
使用-k
選項執行一個指定的用例:
$ pipenv run pytest -q -s -k 163 src/chapter-4/test_params.py .斷開 smtp.163.com:25 1 passed, 1 deselected in 0.16s
使用--collect-only
能夠顯示這些測試ID
,而不執行用例:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_params.py src/chapter-4/test_params.py::test_parames[smtp.163.com] src/chapter-4/test_params.py::test_parames[smtp.126.com] no tests ran in 0.01s
同時,也可使用ids
關鍵字參數,自定義測試ID
:
# src/chapter-4/test_ids.py @pytest.fixture(params=[0, 1], ids=['spam', 'ham']) def a(request): return request.param def test_a(a): pass
執行--collect-only
:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_a src/chapter-4/test_ids.py::test_a[spam] src/chapter-4/test_ids.py::test_a[ham] no tests ran in 0.01s
咱們看到,測試ID
爲咱們指定的值;
數字、字符串、布爾值和None
在測試ID
中使用的是它們的字符串表示形式:
# src/chapter-4/test_ids.py def idfn(fixture_value): if fixture_value == 0: return "eggs" elif fixture_value == 1: return False elif fixture_value == 2: return None else: return fixture_value @pytest.fixture(params=[0, 1, 2, 3], ids=idfn) def b(request): return request.param def test_b(b): pass
執行--collect-only
:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_b src/chapter-4/test_ids.py::test_b[eggs] src/chapter-4/test_ids.py::test_b[False] src/chapter-4/test_ids.py::test_b[2] src/chapter-4/test_ids.py::test_b[3] no tests ran in 0.01s
能夠看到:
ids
能夠接收一個函數,用於生成測試ID
;ID
指定爲None
時,使用的是params
原先對應的值;注意:
當測試
params
中包含元組、字典或者對象時,測試ID
使用的是fixture
函數名+param
的下標:
# src/chapter-4/test_ids.py class C: pass @pytest.fixture(params=[(1, 2), {'d': 1}, C()]) def c(request): return request.param def test_c(c): pass執行
--collect-only
:$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_c src/chapter-4/test_ids.py::test_c[c0] src/chapter-4/test_ids.py::test_c[c1] src/chapter-4/test_ids.py::test_c[c2] no tests ran in 0.01s能夠看到,測試
ID
爲fixture
的函數名(c
)加上對應param
的下標(從0
開始);若是你不想這樣,可使用
str()
方法或者複寫__str__()
方法;
fixture
中標記用例在fixture
的params
參數中,可使用pytest.param
標記這一輪的全部用例,其用法和在pytest.mark.parametrize
中的用法同樣;
# src/chapter-4/test_fixture_marks.py import pytest @pytest.fixture(params=[('3+5', 8), pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')]) def data_set(request): return request.param def test_data(data_set): assert eval(data_set[0]) == data_set[1]
咱們使用pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')
的形式指定一個request.param
入參,其中marks
表示當用例使用這個入參時,爲這個用例打上xfail
標記;而且,咱們還使用id
爲此時的用例指定了一個測試ID
;
$ pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_fixture_marks.py::test_data[data_set0] PASSED [ 50%] src/chapter-4/test_fixture_marks.py::test_data[failed] XFAIL [100%] ======================= 1 passed, 1 xfailed in 0.08s ========================
能夠看到:
XFAIL
,而不是FAILED
;ID
是咱們指定的failed
,而不是data_set1
;咱們也可使用pytest.mark.parametrize
實現相同的效果:
# src/chapter-4/test_fixture_marks.py @pytest.mark.parametrize( 'test_input, expected', [('3+5', 8), pytest.param('6*9', 42, marks=pytest.mark.xfail, id='failed')]) def test_data2(test_input, expected): assert eval(test_input) == expected
執行:
pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data2 ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_fixture_marks.py::test_data2[3+5-8] PASSED [ 50%] src/chapter-4/test_fixture_marks.py::test_data2[failed] XFAIL [100%] ======================= 1 passed, 1 xfailed in 0.07s ========================
fixture
使用其它的fixture
你不只僅能夠在測試用例上使用fixture
,還能夠在fixture
的聲明函數中使用其它的fixture
;這有助於模塊化的設計你的fixture
,能夠在多個項目中重複使用框架級別的fixture
;
一個簡單的例子,咱們能夠擴展以前src/chapter-4/test_params.py
的例子,實例一個app
對象:
# src/chapter-4/test_appsetup.py import pytest class App: def __init__(self, smtp_connection): self.smtp_connection = smtp_connection @pytest.fixture(scope='module') def app(smtp_connection_params): return App(smtp_connection_params) def test_smtp_connection_exists(app): assert app.smtp_connection
咱們建立一個fixture app
並調用以前在conftest.py
中定義的smtp_connection_params
,返回一個App
的實例;
執行:
$ pipenv run pytest -v src/chapter-4/test_appsetup.py ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.163.com] PASSED [ 50%] src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.126.com] PASSED [100%] ============================= 2 passed in 1.25s =============================
由於app
使用了參數化的smtp_connection_params
,因此測試用例test_smtp_connection_exists
會使用不一樣的App
實例執行兩次,而且,app
並不須要關心smtp_connection_params
的實現細節;
app
的做用域是模塊級別的,它又調用了smtp_connection_params
,也是模塊級別的,若是smtp_connection_params
是會話級別的做用域,這個例子仍是同樣能夠正常工做的;這是由於低級別的做用域能夠調用高級別的做用域,可是高級別的做用域調用低級別的做用域會返回一個ScopeMismatch
的異常;
fixture
實例在測試期間,pytest
只激活最少個數的fixture
實例;若是你擁有一個參數化的fixture
,全部使用它的用例會在建立的第一個fixture
實例並銷燬後,纔會去使用第二個實例;
下面這個例子,使用了兩個參數化的fixture
,其中一個是模塊級別的做用域,另外一個是用例級別的做用域,而且使用print
方法打印出它們的setup/teardown
流程:
# src/chapter-4/test_minfixture.py import pytest @pytest.fixture(scope="module", params=["mod1", "mod2"]) def modarg(request): param = request.param print(" SETUP modarg", param) yield param print(" TEARDOWN modarg", param) @pytest.fixture(scope="function", params=[1, 2]) def otherarg(request): param = request.param print(" SETUP otherarg", param) yield param print(" TEARDOWN otherarg", param) def test_0(otherarg): print(" RUN test0 with otherarg", otherarg) def test_1(modarg): print(" RUN test1 with modarg", modarg) def test_2(otherarg, modarg): print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
執行:
$ pipenv run pytest -q -s src/chapter-4/test_minfixture.py SETUP otherarg 1 RUN test0 with otherarg 1 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test0 with otherarg 2 . TEARDOWN otherarg 2 SETUP modarg mod1 RUN test1 with modarg mod1 . SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 . TEARDOWN otherarg 2 TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 . SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 . TEARDOWN otherarg 2 TEARDOWN modarg mod2 8 passed in 0.02s
能夠看出:
mod1
的TEARDOWN
操做完成後,纔開始mod2
的SETUP
操做;test_0
獨立完成測試;test_1
和test_2
都使用到了模塊級別的modarg
,同時test_2
也使用到了用例級別的otherarg
。它們執行的順序是,test_1
先使用mod1
,接着test_2
使用mod1
和otherarg 1/otherarg 2
,而後test_1
使用mod2
,最後test_2
使用mod2
和otherarg 1/otherarg 2
;也就是說test_1
和test_2
共用相同的modarg
實例,最少化的保留fixture
的實例個數;fixture
實例有時,咱們並不須要在測試用例中直接使用fixture
實例;例如,咱們須要一個空的目錄做爲當前用例的工做目錄,可是咱們並不關心如何建立這個空目錄;這裏咱們可使用標準的tempfile模塊來實現這個功能;
# src/chapter-4/conftest.py import pytest import tempfile import os @pytest.fixture() def cleandir(): newpath = tempfile.mkdtemp() os.chdir(newpath)
在測試中使用usefixtures
標記聲明使用它:
# src/chapter-4/test_setenv.py import os import pytest @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: 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
標記,測試類TestDirectoryInit
中全部的測試用例均可以使用cleandir
,這和在每一個測試用例中指定cleandir
參數是同樣的;
執行:
$ pipenv run pytest -q -s src/chapter-4/test_setenv.py .. 2 passed in 0.02s
你可使用以下方式指定多個fixture
:
@pytest.mark.usefixtures("cleandir", "anotherfixture") def test(): ...
你也可使用以下方式爲測試模塊指定fixture
:
pytestmark = pytest.mark.usefixtures("cleandir")
注意:參數的名字必須是
pytestmark
;
你也可使用以下方式爲整個項目指定fixture
:
# src/chapter-4/pytest.ini [pytest] usefixtures = cleandir
注意:
usefixtures
標記不適用於fixture
聲明函數;例如:@pytest.mark.usefixtures("my_other_fixture") @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture(): ...這並不會返回任何的錯誤或告警,具體討論能夠參考#3664
fixture
有時候,你想在測試用例中自動使用fixture
,而不是做爲參數使用或者usefixtures
標記;設想,咱們有一個數據庫相關的fixture
,包含begin/rollback/commit
的體系結構,如今咱們但願經過begin/rollback
包裹每一個測試用例;
下面,經過列表實現一個虛擬的例子:
# src/chapter-4/test_db_transact.py import pytest class DB: 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: @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"]
類級別做用域的transact
函數中聲明瞭autouse=True
,因此TestClass
中的全部用例,能夠自動調用transact
而不用顯式的聲明或標記;
執行:
$ pipenv run pytest -q -s src/chapter-4/test_db_transact.py .. 2 passed in 0.01s
autouse=True
的fixture
在其它級別做用域中的工做流程:
autouse fixture
遵循scope
關鍵字的定義:若是其含有scope='session'
,則無論它在哪裏定義的,都將只執行一次;scope='class'
表示每一個測試類執行一次;autouse fixture
,那麼這個測試模塊全部的用例自動使用它;conftest.py
中定義autouse fixture
,那麼它的相同文件夾和子文件夾中的全部測試模塊中的用例都將自動使用它;autouse fixture
,那麼全部安裝這個插件的項目中的全部用例都將自動使用它;上述的示例中,咱們指望只有TestClass
的用例自動調用fixture transact
,這樣咱們就不但願transact
一直處於激活的狀態,因此更標準的作法是,將transact
聲明在conftest.py
中,而不是使用autouse=True
:
@pytest.fixture def transact(request, db): db.begin() yield db.rollback()
而且,在TestClass
上聲明:
@pytest.mark.usefixtures("transact") class TestClass: def test_method1(self): ...
其它類或者用例也想使用的話,一樣須要顯式的聲明usefixtures
;
fixture
在大型的測試中,你可能須要在本地覆蓋項目級別的fixture
,以增長可讀性和便於維護;
conftest.py
)層級覆寫fixture
假設咱們有以下的測試項目:
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'
能夠看到:
conftest.py
中的fixture
覆蓋了上層文件夾中同名的fixture
;conftest.py
中的fixture
能夠輕鬆的訪問上層文件夾中同名的fixture
;fixture
假設咱們有以下的測試項目:
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 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
中同名的fixture
;fixture
能夠輕鬆的訪問conftest.py
中同名的fixture
;fixture
假設咱們有以下的測試項目:
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
的值被用例的參數所覆蓋;test_username_other
沒有使用username
,可是other_username
使用到了username
,因此也一樣受到了影響;fixture
覆寫非參數化的fixture
,反之亦然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'
能夠看出:
fixture
和非參數化的fixture
一樣能夠相互覆蓋;