pytest封神之路第三步 精通fixture
首先放一句「狠話」。
如果你不會fixture,那麼你最好別說自己會pytest。
(只是為了烘托主題哈,手上的磚頭可以放下了,手動滑稽)
fixture是什麼
看看源碼
def fixture(
callable_or_scope=None,
*args,
scope="function",
params=None,
autouse=False,
ids=None,
name=None
):
"""Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a
fixture function.
The name of the fixture function can later be referenced to cause its
invocation ahead of running tests: test
modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
marker.
Test functions can directly use fixture names as input
arguments in which case the fixture instance returned from the fixture
function will be injected.
Fixtures can provide their values to test functions using ``return`` or ``yield``
statements. When using ``yield`` the code block after the ``yield`` statement is executed
as teardown code regardless of the test outcome, and must yield exactly once.
:arg scope: the scope for which this fixture is shared, one of
``"function"`` (default), ``"class"``, ``"module"``,
``"package"`` or ``"session"`` (``"package"`` is considered **experimental**
at this time).
This parameter may also be a callable which receives ``(fixture_name, config)``
as parameters, and must return a ``str`` with one of the values mentioned above.
See :ref:`dynamic scope` in the docs for more information.
:arg params: an optional list of parameters which will cause multiple
invocations of the fixture function and all of the tests
using it.
The current parameter is available in ``request.param``.
:arg autouse: if True, the fixture func is activated for all tests that
can see it. If False (the default) then an explicit
reference is needed to activate the fixture.
:arg ids: list of string ids each corresponding to the params
so that they are part of the test id. If no ids are provided
they will be generated automatically from the params.
:arg name: the name of the fixture. This defaults to the name of the
decorated function. If a fixture is used in the same module in
which it is defined, the function name of the fixture will be
shadowed by the function arg that requests the fixture; one way
to resolve this is to name the decorated function
``fixture_<fixturename>`` and then use
``@pytest.fixture(name='<fixturename>')``.
"""
if params is not None:
params = list(params)
fixture_function, arguments = _parse_fixture_args(
callable_or_scope,
*args,
scope=scope,
params=params,
autouse=autouse,
ids=ids,
name=name,
)
scope = arguments.get("scope")
params = arguments.get("params")
autouse = arguments.get("autouse")
ids = arguments.get("ids")
name = arguments.get("name")
if fixture_function and params is None and autouse is False:
# direct decoration
return FixtureFunctionMarker(scope, params, autouse, name=name)(
fixture_function
)
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
總結一下
【定義】
- fixture是一個函數,在函數上添加註解
@pytest.fixture
來定義 - 定義在conftest.py中,無需import就可以調用
- 定義在其他文件中,import後也可以調用
- 定義在相同文件中,直接調用
【使用】
- 第一種使用方式是
@pytest.mark.usefixtures(fixturename)
(如果修飾TestClass能對類中所有方法生效) - 第二種使用方式是作為函數參數
- 第三種使用方式是autouse(不需要顯示調用,自動運行)
conftest.py
我們常常會把fixture定義到conftest.py文件中。
這是pytest固定的文件名,不能自定義。
必須放在package下,也就是目錄中有__init__.py。
conftest.py中的fixture可以用在當前目錄及其子目錄,不需要import,pytest會自動找。
可以創建多個conftest.py文件,同名fixture查找時會優先用最近的。
依賴注入
fixture實現了依賴注入。依賴注入是控制反轉(IoC, Inversion of Control)的一種技術形式。
簡單理解一下什麼是依賴注入和控制反轉
實在是妙啊!我們可以在不修改當前函數程式碼邏輯的情況下,通過fixture來額外添加一些處理。
入門示例
# content of ./test_smtpsimple.py
import smtplib
import pytest
@pytest.fixture
def smtp_connection():
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
執行後程式處理邏輯
- pytest找到test_開頭的函數,發現需要名字為smtp_connection的fixture,就去找
- 找到之後,調用smtp_connection(),return了SMTP的實例
- 調用test_ehlo(<smtp_connection instance>) ,入參
smtp_connection
等於fixture return的值
如果想看文件定義了哪些fixture,可以使用命令,_前綴的需要跟上-v
pytest --fixtures test_simplefactory.py
fixture scope & order
既然到處都可以定義fixture,那多了豈不就亂了?
pytest規定了fxture的運行範圍和運行順序。
fixture的範圍通過參數scope來指定
@pytest.fixture(scope="module")
默認是function,可以選擇function, class, module, package 或 session。
fixture都是在test第一次調用時創建,根據scope的不同有不同的運行和銷毀方式
- function 每個函數運行一次,函數結束時銷毀
- class 每個類運行一次,類結束時銷毀
- module 每個模組運行一次,模組結束時銷毀
- package 每個包運行一次,包結束時銷毀
- session 每個會話運行一次,會話結束時銷毀
fixture的順序優先按scope從大到小,session > package > module > class > function。
如果scope相同,就按test調用先後順序,以及fixture之間的依賴關係。
autouse的fixture會優先於相同scope的其他fixture。
示例
import pytest
# fixtures documentation order example
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"]
雖然test_order()是按f1, m1, f2, s1調用的,但是結果卻不是按這個順序
- s1 scope為session
- m1 scope為module
- a1 autouse,默認function,後於session、module,先於function其他fixture
- f3 被f1依賴
- f1 test_order()參數列表第1個
- f2 test_order()參數列表第3個
fixture嵌套
fixture裝飾的是函數,那函數也有入參咯。
fixture裝飾的函數入參,只能是其他fixture。
示例,f1依賴f3,如果不定義f3的話,執行會報錯fixture ‘f3’ not found
@pytest.fixture
def f1(f3):
order.append("f1")
@pytest.fixture
def f3():
order.append("f3")
def test_order(f1):
pass
從test傳值給fixture
藉助request,可以把test中的值傳遞給fixture。
示例1,smtp_connection可以使用module中的smtpserver
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print("finalizing {} ({})".format(smtp_connection, server))
smtp_connection.close()
# 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()
示例2,結合request+mark,把fixt_data從test_fixt傳值給了fixt
import pytest
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
# Do something with the data
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
fixture setup / teardown
其他測試框架unittest/testng,都定義了setup和teardown函數/方法,用來測試前初始化和測試後清理。
pytest也有,不過是兼容unittest等弄的,不推薦!
from loguru import logger
def setup():
logger.info("setup")
def teardown():
logger.info("teardown")
def test():
pass
建議使用fixture。
setup,fixture可以定義autouse來實現初始化。
@pytest.fixture(autouse=True)
autouse的fixture不需要調用,會自己運行,和test放到相同scope,就能實現setup的效果。
autouse使用說明
- autouse遵循scope的規則,scope=”session”整個會話只會運行1次,其他同理
- autouse定義在module中,module中的所有function都會用它(如果scope=”module”,只運行1次,如果scope=”function”,會運行多次)
- autouse定義在conftest.py,conftest覆蓋的test都會用它
- autouse定義在plugin中,安裝plugin的test都會用它
- 在使用autouse時需要同時注意scope和定義位置
示例,transact默認scope是function,會在每個test函數執行前自動運行
# content of 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"]
這個例子不用autouse,用conftest.py也能實現
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
@pytest.mark.usefixtures("transact")
class TestClass:
def test_method1(self):
...
teardown,可以在fixture中使用yield關鍵字來實現清理。
示例,scope為module,在module結束時,會執行yield後面的print()和smtp_connection.close()
# content of conftest.py
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()
可以使用with關鍵字進一步簡化,with會自動清理上下文,執行smtp_connection.close()
# content of test_yield2.py
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
fixture參數化
後續會專門講「pytest參數化」,這裡就先跳過,請各位見諒啦。
因為我覺得想用pytest做參數化,一定是先到參數化的文章裡面找,而不是到fixture。
把這部分放到參數化,更便於以後檢索。
簡要回顧
本文開頭通過源碼介紹了fixture是什麼,並簡單總結定義和用法。然後對依賴注入進行了解釋,以更好理解fixture技術的原理。入門示例給出了官網的例子,以此展開講了範圍、順序、嵌套、傳值,以及初始化和清理的知識。
如果遇到問題,歡迎溝通討論。
更多實踐內容,請關注後續篇章《tep最佳實踐》。
參考資料
//en.wikipedia.org/wiki/Dependency_injection
//en.wikipedia.org/wiki/Inversion_of_control
//docs.pytest.org/en/stable/contents.html#toc
版權申明:本文為部落客原創文章,轉載請保留原文鏈接及作者。
如果您喜歡我寫的文章,請關注公眾號支援一下,謝謝哈哈哈。