這篇文章接著上一篇《探索pytest的fixture(上)》的內容講。
使用fixture函數的fixture
我們不僅可以在測試函數中使用fixture,而且fixture函數也可以使用其他fixture,這有助于fixture的模塊化設計,并允許在許多項目中重新使用框架特定的fixture。例如,我們可以擴展前面的例子,并實例化一個app
對象,我們把已經定義好的smtp
資源粘貼到其中,新建一個test_appsetup.py文件,輸入以下代碼:
import pytest
class App(object):
def __init__(self, smtp):
self.smtp = smtp
@pytest.fixture(scope="module")
def app(smtp):
return App(smtp)
def test_smtp_exists(app):
assert app.smtp
在這里,我們聲明一個app
fixture,用來接收之前定義的smtp
fixture,并用它實例化一個App
對象,讓我們來運行它:
由于smtp
的參數化,測試將運行兩次不同的App
實例和各自的smtp服務器。pytest將完全分析fixture依賴關系圖,因此app
fixture不需要知道smtp
參數化。
還要注意一下的是,app
fixture具有一個module
(模塊)范圍,并使用module
(模塊)范圍的smtp
fixture。如果smtp
被緩存在一個session
(會話)范圍內,這個例子仍然可以工作,fixture使用更大范圍內的fixture是好的,但是不能反過來,session
(會話)范圍的fixture不能以有意義的方式使用module
(模塊)范圍的fixture。
通過fixture實例自動分組測試
pytest在測試運行期間會最小化活動fixture的數量,如果我們有一個參數化的fixture,那么所有使用它的測試將首先執行一個實例,然后在下一個fixture實例被創建之前調用終結器。除此之外,這可以簡化對創建和使用全局狀態的應用程序的測試。
以下示例使用兩個參數化fixture,其中一個作用于每個模塊,所有功能都執行print
調用來顯示設置流程,修改之前的test_module.py文件,輸入以下代碼:
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print (" 設置 modarg %s" % param)
yield param
print (" 拆卸 modarg %s" % param)
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print (" 設置 otherarg %s" % param)
yield param
print (" 拆卸 otherarg %s" % param)
def test_0(otherarg):
print (" 用 otherarg %s 運行 test0" % otherarg)
def test_1(modarg):
print (" 用 modarg %s 運行 test1" % modarg)
def test_2(otherarg, modarg):
print (" 用 otherarg %s 和 modarg %s 運行 test2" % (otherarg, modarg))
讓我們使用pytest -v -s test_module.py
運行詳細模式測試并查看打印輸出:
我們可以看到參數化的module
(模塊)范圍的modarg
資源影響了測試執行的排序,使用了最少的活動資源。mod1
參數化資源的終結器是在mod2
資源建立之前執行的。特別要注意test_0
是完全獨立的,會首先完成,然后用mod1
執行test_1
,再然后用mod1
執行test_2
,再然后用mod2
執行test_1
,最后用mod2
執行test_2
。otherarg
參數化資源是function
(函數)的范圍,是在每次使用測試之后建立起來的。
使用類、模塊或項目的fixture
有時測試函數不需要直接訪問一個fixture對象,例如,測試可能需要使用空目錄作為當前工作目錄,不關心具體目錄。這里使用標準的tempfile和pytest fixture來實現它,我們將fixture的創建分隔成一個conftest.py文件:
import pytest
import tempfile
import os
@pytest.fixture()
def cleandir():
newpath = tempfile.mkdtemp()
os.chdir(newpath)
并通過usefixtures
標記聲明在測試模塊中的使用,新建一個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
標記,所以執行每個測試方法都需要cleandir
fixture,就像為每個測試方法指定了一個cleandir
函數參數一樣,讓我們運行它來驗證我們的fixture已激活:
我們可以像這樣指定多個fixture:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
我們可以使用標記機制的通用功能來指定測試模塊級別的fixture使用情況:
pytestmark = pytest.mark.usefixtures("cleandir")
要注意的是,分配的變量必須被稱為pytestmark
,例如,分配foomark
不會激活fixture。最后,我們可以將項目中所有測試所需的fixture放入一個pytest.ini
文件中:
[pytest]
usefixtures = cleandir
自動使用fixture
有時候,我們可能希望自動調用fixture,而不是顯式聲明函數參數或使用usefixtures
裝飾器,例如,我們有一個數據庫fixture,它有一個開始、回滾、提交的體系結構,我們希望通過一個事務和一個回滾自動地包含每一個測試方法,新建一個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"]
類級別的transact
fixture被標記為autouse=true
,這意味著類中的所有測試方法將使用該fixture,而不需要在測試函數簽名或類級別的usefixtures
裝飾器中陳述它。如果我們運行它,會得到兩個通過的測試:
以下是在其他范圍內如何使用自動fixture:
- 自動fixture遵循
scope=
關鍵字參數,如果一個自動fixture的scope='session'
,它將只運行一次,不管它在哪里定義。scope='class'
表示每個類會運行一次,等等。 - 如果在一個測試模塊中定義一個自動fixture,所有的測試函數都會自動使用它。
- 如果在
conftest.py
文件中定義了自動fixture,那么在其目錄下的所有測試模塊中的所有測試都將調用fixture。 - 最后,要小心使用自動fixture,如果我們在插件中定義了一個自動fixture,它將在插件安裝的所有項目中的所有測試中被調用。例如,在
pytest.ini
文件中,這樣一個全局性的fixture應該真的應該做任何工作,避免無用的導入或計算。
最后還要注意,上面的的transact
fixture可能是我們希望在項目中提供的fixture,而沒有通常的激活,規范的方法是將定義放在conftest.py
文件而不使用自動運行:
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
然后例如,有一個測試類通過聲明使用它需要:
@pytest.mark.usefixtures("transact")
class TestClass(object):
def test_method1(self):
......
在這個測試類中的所有測試方法將使用transact
fixture,而模塊中的其他測試類或函數將不會使用它,除非它們也添加一個transact
引用。
覆蓋不同級別的fixture
在相對較大的測試套件中,我們很可能需要使用本地定義的套件重寫全局或根fixture,從而保持測試代碼的可讀性和可維護性。
覆蓋文件夾級別的fixture
鑒于測試文件的結構是:
tests/
__init__.py
conftest.py
# tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
正如上面代碼所示,具有相同名稱的fixture可以在某些測試文件夾級別上被覆蓋,但是要注意的是,base
或super
fixture可以輕松地從上面的fixture進入,并在上面的例子中使用。
覆蓋測試模塊級別的fixture
鑒于測試文件的結構是:
tests/
__init__.py
conftest.py
# tests/conftest.py
@pytest.fixture
def username():
return 'username'
test_something.py
# 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
# 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。
直接用測試參數化覆蓋fixture
鑒于測試文件的結構是:
tests/
__init__.py
conftest.py
# tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# 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值被測試參數值覆蓋,即使測試不直接使用它,fixture的值也可以用這種方式重寫。
使用非參數化參數替代參數化的fixture
鑒于測試文件的結構是:
tests/
__init__.py
conftest.py
# 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
# 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
# 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被某個測試模塊的參數化版本覆蓋,這同樣適用于測試文件夾級別。