pytest 精通 fixture

dongfanger發表於2020-09-23

首先放一句“狠話”。

如果你不會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

執行後程式處理邏輯

  1. pytest找到test_開頭的函式,發現需要名字為smtp_connection的fixture,就去找
  2. 找到之後,呼叫smtp_connection(),return了SMTP的例項
  3. 呼叫test_ehlo() ,入參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呼叫的,但是結果卻不是按這個順序

  1. s1 scope為session
  2. m1 scope為module
  3. a1 autouse,預設function,後於session、module,先於function其他fixture
  4. f3 被f1依賴
  5. f1 test_order()引數列表第1個
  6. 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最佳實踐》。

參考資料

https://en.wikipedia.org/wiki/Dependency_injection

https://en.wikipedia.org/wiki/Inversion_of_control

https://docs.pytest.org/en/stable/contents.html#toc

相關文章