概述
pytest-lazy-fixtures 是一个 pytest 插件,它允许你以惰性(延迟)的方式使用 fixtures。这意味着 fixtures 将不会在测试开始时立即执行,而是在真正需要它们的时候才进行加载和执行。这种特性对于以下几种情况特别有用:
-
减少资源消耗:当
fixtures的设置或拆除过程成本高昂时,延迟它们的执行可以节省资源。 -
避免不必要的执行:如果某些
fixtures只在特定条件下需要,惰性加载可以避免在不需要时执行它们。 -
提高测试速度:通过延迟非必要的
fixtures加载,可以减少测试套件的整体启动时间。 -
复杂的测试依赖:在测试依赖于多个复杂的
fixtures时,可以更灵活地控制它们的加载顺序。 -
参数化测试:它支持与参数化测试结合使用,允许更复杂的测试场景定义。
使用 pytest-lazy-fixtures 插件,你可以定义一个 fixture,然后在测试函数中以惰性方式引用它。当测试运行到需要该 fixture 的时候,它才会被加载和执行。
安装 pytest-lazy-fixtures 插件非常简单,可以通过 pip 进行安装:
pip install pytest-lazy-fixtures
使用插件后,你可以通过 lazy_fixture 函数来引用 fixtures,该函数接受一个或多个 fixture 名称作为参数,并返回一个或多个 LazyFixture 对象。在测试函数中使用这些对象,pyteset 会在适当的时候解析并加载相应的 fixtures。
例如:
import pytest
from pytest_lazy_fixtures import lazy_fixture
@pytest.mark.usefixtures("db_fixture")
def test_example(lazy_fixture("db_session")):
# 在这里,db_session fixture 将被延迟加载
session = lazy_fixture("db_session") # 获取实际的 fixture 实例
# 使用 session 进行测试...
在这个例子中,db_session fixture 只有在 test_example 测试函数实际需要它时才会被加载。如果测试函数有多个参数,所有参数都将按需加载。这为测试提供了更高的灵活性和效率。
源码解读
源码目录结构
从官网下载解压到本地:
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1# ll
total 36
drwxr-xr-x 4 root root 4096 Jul 24 13:46 ./
drwxr-xr-x 21 root root 4096 Jul 24 13:46 ../
-rw-r--r-- 1 root root 1069 Jul 23 05:39 LICENSE
-rw-r--r-- 1 root root 4502 Jan 1 1970 PKG-INFO
-rw-r--r-- 1 root root 1666 Jul 23 05:39 pyproject.toml
drwxr-xr-x 2 root root 4096 Jul 24 13:46 pytest_lazy_fixtures/
-rw-r--r-- 1 root root 3683 Jul 23 05:39 README.md
drwxr-xr-x 2 root root 4096 Jul 24 13:46 tests/
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1# cd pytest_lazy_fixtures/
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1/pytest_lazy_fixtures# ll
total 32
drwxr-xr-x 2 root root 4096 Jul 24 13:46 ./
drwxr-xr-x 4 root root 4096 Jul 24 13:46 ../
-rw-r--r-- 1 root root 93 Jul 23 05:39 __init__.py
-rw-r--r-- 1 root root 966 Jul 23 05:39 lazy_fixture_callable.py
-rw-r--r-- 1 root root 839 Jul 23 05:39 lazy_fixture.py
-rw-r--r-- 1 root root 864 Jul 23 05:39 loader.py
-rw-r--r-- 1 root root 3322 Jul 23 05:39 normalizer.py
-rw-r--r-- 1 root root 600 Jul 23 05:39 plugin.py
-rw-r--r-- 1 root root 0 Jul 23 05:39 py.typed
root@Gavin:~/pytest_plugin/pytest_lazy_fixtures-1.1.1/pytest_lazy_fixtures#
lazy_fixture.py文件
from dataclasses import dataclass
from operator import attrgetter
from typing import Optional
import pytest
@dataclass
class LazyFixtureWrapper:
name: str
@property
def fixture_name(self) -> str:
return self.name.split(".", maxsplit=1)[0]
def _get_attr(self, fixture) -> Optional[str]:
splitted = self.name.split(".", maxsplit=1)
if len(splitted) == 1:
return fixture
return attrgetter(splitted[1])(fixture)
def __repr__(self) -> str:
return self.name
def load_fixture(self, request: pytest.FixtureRequest):
return self._get_attr(request.getfixturevalue(self.fixture_name))
def __hash__(self) -> int:
return hash(self.name)
def lf(name: str) -> LazyFixtureWrapper:
"""lf is a lazy fixture."""
return LazyFixtureWrapper(name)
逻辑说明
-
LazyFixtureWrapper类封装了延迟加载fixture的逻辑,允许用户通过字符串名称引用fixture,并在实际使用时才加载其值。 -
load_fixture方法是核心,它使用pytest的request对象来获取fixture的值,并根据需要获取其属性。 -
lf函数提供了一个简洁的接口,用于创建LazyFixtureWrapper实例。
详细介绍如下:
导入依赖:
-
导入 Python 标准库中的
dataclasses模块,用于创建数据类。 -
导入
operator模块中的attrgetter函数,用于获取对象的属性。 -
导入
typing模块中的Optional类型注解。 -
导入
pytest模块,用于与pytest框架集成。
定义 LazyFixtureWrapper 数据类:
-
使用
@dataclass装饰器定义了一个名为LazyFixtureWrapper的数据类,用于包装延迟加载的fixture。 -
name: 一个字符串属性,存储fixture的名称或其路径(如果需要访问fixture的属性)。
fixture_name 属性:
-
一个只读属性,返回
name属性的第一部分(通过.分隔),这通常是fixture的基本名称。
. _get_attr 方法:
-
一个私有方法,如果
name只包含一个部分,直接返回fixture;如果包含多个部分,使用attrgetter获取fixture对象的指定属性。
__repr__ 方法:
-
定义了类的字符串表示方法,返回
name属性的值。
load_fixture 方法:
-
一个公共方法,接受一个
request参数(pytest.FixtureRequest类型),调用request.getfixturevalue方法获取fixture的值,然后使用_get_attr方法获取最终的属性值。
__hash__ 方法:
-
定义了类的哈希方法,基于
name属性的哈希值。
lf 函数:
-
一个工厂函数,接受一个字符串参数
name,创建并返回一个新的LazyFixtureWrapper实例。
示例
假设有一个 fixture db_session,你可能想在测试中引用它,但只在实际需要时才加载它:
def test_query(lf("db_session.my_attribute")):
# 只有在这一行时,db_session 及其 my_attribute 才会被加载
my_attribute = lf("db_session.my_attribute").load_fixture(request)
# 使用 my_attribute 进行测试...
在这个例子中,lf 函数用于创建一个延迟加载的 fixture 引用,只有当 load_fixture 被调用时,db_session 和它的 my_attribute 属性才会被实际加载。这有助于减少不必要的资源消耗和初始化。
lazy_fixture_callable.py文件
from typing import Callable, Optional, Union
import pytest
from .lazy_fixture import LazyFixtureWrapper
class LazyFixtureCallableWrapper(LazyFixtureWrapper):
_func: Optional[Callable]
args: tuple
kwargs: dict
def __init__(self, func_or_name: Union[Callable, str], *args, **kwargs):
if callable(func_or_name):
self._func = func_or_name
self.name = func_or_name.__name__
else:
self.name = func_or_name
self._func = None
self.args = args
self.kwargs = kwargs
def get_func(self, request: pytest.FixtureRequest) -> Callable:
func = self._func
if func is None:
func = self.load_fixture(request)
assert callable(func)
return func
def lfc(name: Union[Callable, str], *args, **kwargs) -> LazyFixtureCallableWrapper:
"""lfc is a lazy fixture callable."""
return LazyFixtureCallableWrapper(name, *args, **kwargs)
这段代码提供了一种方式来延迟调用 fixtures 或任意可调用对象,直到它们真正需要被使用时。
逻辑说明
-
LazyFixtureCallableWrapper类封装了延迟调用fixture或任意可调用对象的逻辑。 -
如果提供的名称是一个可调用对象,它将被直接存储;如果是一个字符串,则在需要时通过
pytest的fixture系统加载。 -
get_func方法用于获取实际的函数对象,如果之前没有提供函数,则通过加载fixture获取。 -
lfc函数提供了一个简洁的接口,用于创建LazyFixtureCallableWrapper实例。
详细介绍如下:
导入依赖:
-
导入
Callable,Optional,Union等类型注解,用于注解函数和方法的参数和返回值。 -
导入
pytest模块,用于与pytest框架集成。 -
从
lazy_fixture模块导入LazyFixtureWrapper类。
定义 LazyFixtureCallableWrapper 类:
-
LazyFixtureCallableWrapper类继承自LazyFixtureWrapper。
类属性:
-
_func: 用于存储延迟调用的函数或 None。 -
args: 存储传递给延迟调用函数的位置参数。 -
kwargs: 存储传递给延迟调用函数的关键字参数。
构造函数:
-
接受
func_or_name参数,它可以是一个可调用对象或一个字符串名称。 -
如果
func_or_name是可调用的,将其存储在_func中,并使用__name__属性作为name。 -
如果
func_or_name不是可调用的,假定它是一个 fixture 名称,存储在name中,并将_func设置为 None。 -
args和kwargs分别存储构造函数的位置参数和关键字参数。
get_func 方法:
-
接受一个
request参数(pytest.FixtureRequest类型)。 -
如果
_func是 None,则使用load_fixture方法加载fixture,并断言加载的结果是可调用的。 -
返回加载的函数或之前存储的
_func。
lfc 函数:
-
一个工厂函数,接受
name参数(可以是函数或字符串名称),以及任意数量的位置参数和关键字参数。 -
创建并返回一个新的
LazyFixtureCallableWrapper实例。
示例
假设有一个 fixture db_session,你可能想延迟调用它,直到实际需要执行数据库操作:
def test_query(lfc(db_session)):
# 只有在这一行时,db_session 才会被加载和调用
db = lfc(db_session).get_func(request)
result = db.execute("SELECT * FROM table")
# 使用 result 进行测试...
在这个例子中,lfc 函数用于创建一个延迟调用的 fixture 引用,只有当 get_func 被调用时,db_session 才会被实际加载和调用。这有助于减少不必要的初始化和延迟执行。
loader.py文件
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def load_lazy_fixtures(value, request: pytest.FixtureRequest):
if isinstance(value, LazyFixtureCallableWrapper):
return value.get_func(request)(
*load_lazy_fixtures(value.args, request),
**load_lazy_fixtures(value.kwargs, request),
)
if isinstance(value, LazyFixtureWrapper):
return value.load_fixture(request)
# we need to check exact type
if type(value) is dict: # noqa: E721
return {load_lazy_fixtures(key, request): load_lazy_fixtures(value, request) for key, value in value.items()}
# we need to check exact type
elif type(value) in {list, tuple, set}:
return type(value)([load_lazy_fixtures(value, request) for value in value])
return value
递归地加载延迟加载的 fixtures。
代码逻辑
-
load_lazy_fixtures函数负责处理延迟加载的fixtures,包括LazyFixtureWrapper和LazyFixtureCallableWrapper实例。 -
它递归地加载
value中的fixtures,无论是作为位置参数、关键字参数,还是嵌套在字典、列表、元组或集合中。 -
函数使用类型检查(
isinstance)来确定如何处理value,对于dict、list、tuple和set,它使用确切的类型检查(type),以避免错误地处理继承自这些类型的自定义类型。
详细信息描述如下:
导入依赖:
-
导入
pytest模块,用于与pytest框架集成。 -
从
lazy_fixture模块导入LazyFixtureWrapper类,用于包装延迟加载的fixture。 -
从
lazy_fixture_callable模块导入LazyFixtureCallableWrapper类,用于包装延迟调用的fixture。
load_lazy_fixtures 函数:
-
函数接受两个参数:
value,可能是延迟加载的fixture或其他值;request,pytest的FixtureRequest对象,用于访问fixtures。 -
函数首先检查
value是否是LazyFixtureCallableWrapper的实例。如果是,调用get_func方法获取实际的函数,并使用load_lazy_fixtures递归加载args和kwargs,然后调用该函数。 -
接下来,检查
value是否是LazyFixtureWrapper的实例。如果是,调用load_fixture方法加载fixture。 -
然后,检查
value是否是字典(dict)的确切类型。如果是,递归地加载字典的键和值。 -
检查
value是否是列表(list)、元组(tuple)或集合(set)的确切类型。如果是,递归地加载这些可迭代对象中的每个元素,并返回相应的类型。 -
如果
value不是上述任何一种类型,直接返回value。
示例
假设你有一些延迟加载的 fixtures 和一个需要这些 fixtures 的测试函数:
def test_example(lfc(db_session)):
# 加载 fixtures 并执行测试逻辑
session = load_lazy_fixtures(lfc(db_session), request)
# 使用 session 进行测试...
在这个例子中,lfc(db_session) 创建了一个延迟加载的 fixture 引用。load_lazy_fixtures 函数用于在需要时加载这个 fixture,并可以处理更复杂的参数,如字典、列表等,确保所有延迟加载的 fixtures 都被正确加载。
normalizer.py文件
import copy
from typing import Any, Dict, List, Tuple
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value) -> Tuple[List[str], Dict[str, Any]]:
if isinstance(value, LazyFixtureCallableWrapper):
extra_fixturenames_args, arg2fixturedefs_args = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.args,
)
extra_fixturenames_kwargs, arg2fixturedefs_kwargs = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.kwargs,
)
return [*extra_fixturenames_args, *extra_fixturenames_kwargs], {
**arg2fixturedefs_args,
**arg2fixturedefs_kwargs,
}
if isinstance(value, LazyFixtureWrapper):
if pytest.version_tuple >= (8, 0, 0):
fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure(metafunc.definition.parent, [value.name], {})
else: # pragma: no cover
# TODO: add tox
_, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([value.name], metafunc.definition.parent)
return fixturenames_closure, arg2fixturedefs
extra_fixturenames, arg2fixturedefs = [], {}
# we need to check exact type
if type(value) is dict: # noqa: E721
value = list(value.values())
# we need to check exact type
if type(value) in {list, tuple, set}:
for val in value:
ef, arg2f = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, val)
extra_fixturenames.extend(ef)
arg2fixturedefs.update(arg2f)
return extra_fixturenames, arg2fixturedefs
def normalize_metafunc_calls(metafunc, used_keys=None):
newcalls = []
for callspec in metafunc._calls:
calls = _normalize_call(callspec, metafunc, used_keys)
newcalls.extend(calls)
metafunc._calls = newcalls
def _copy_metafunc(metafunc):
copied = copy.copy(metafunc)
copied.fixturenames = copy.copy(metafunc.fixturenames)
copied._calls = []
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
return copied
def _normalize_call(callspec, metafunc, used_keys):
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
used_keys = used_keys or set()
params = callspec.params.copy() if pytest.version_tuple >= (8, 0, 0) else {**callspec.params, **callspec.funcargs}
valtype_keys = params.keys() - used_keys
for arg in valtype_keys:
value = params[arg]
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
if fixturenames_closure and arg2fixturedefs:
extra_fixturenames = [fname for fname in fixturenames_closure if fname not in params]
newmetafunc = _copy_metafunc(metafunc)
newmetafunc.fixturenames = extra_fixturenames
newmetafunc._arg2fixturedefs.update(arg2fixturedefs)
newmetafunc._calls = [callspec]
fm.pytest_generate_tests(newmetafunc)
normalize_metafunc_calls(newmetafunc, used_keys | {arg})
return newmetafunc._calls
used_keys.add(arg)
return [callspec]
_get_fixturenames_closure_and_arg2fixturedefs方法
_get_fixturenames_closure_and_arg2fixturedefs 函数递归地处理输入值,以提取与 pytest fixtures 相关的信息。这个函数的目的是找出所有由延迟加载的 fixtures 引用所隐含的 fixtures 名称,并收集它们的详细信息。
代码逻辑:
-
函数首先检查
value是否是LazyFixtureCallableWrapper或LazyFixtureWrapper的实例,并相应地递归处理。 -
对于
LazyFixtureCallableWrapper,处理附加的位置参数和关键字参数。 -
对于
LazyFixtureWrapper,调用fm.getfixtureclosure获取fixtures的名称闭包和详细信息。 -
对于字典、列表、元组或集合,递归地处理其中的每个元素,以提取所有相关的
fixtures信息。 -
函数通过递归和类型检查,确保所有可能包含
fixtures引用的地方都被检查并处理。
详细介绍如下:
-
函数签名:
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value): 定义了函数及其参数:fm:pytest的funcmanage插件实例,用于管理fixtures。metafunc: 代表当前测试函数的元数据的对象。value: 可能是LazyFixtureWrapper或LazyFixtureCallableWrapper的实例,或者是要检查的值。
-
返回类型注解:
-> Tuple[List[str], Dict[str, Any]]: 函数返回一个元组,包含一个字符串列表(fixtures名称的闭包)和一个字典(fixtures的详细信息)。
-
处理 LazyFixtureCallableWrapper:
- 如果
value是LazyFixtureCallableWrapper的实例,递归地处理args和kwargs,合并结果并返回。
- 如果
-
处理 LazyFixtureWrapper:
- 如果
value是LazyFixtureWrapper的实例,根据pytest的版本使用不同的方法从fm获取fixtures的名称闭包和详细信息。
- 如果
-
版本兼容处理:
- 根据
pytest的版本(8.0.0 及以上),使用不同的参数顺序调用fm.getfixtureclosure。
- 根据
-
返回 fixtures 信息:
- 对于
LazyFixtureWrapper,返回获取的 `fixtures 名称闭包和详细信息。
- 对于
-
处理字典:
- 如果
value是字典的确切类型,将值转换为列表并递归处理。
- 如果
-
处理列表、元组、集合:
- 如果
value是列表、元组或集合的确切类型,遍历每个元素,递归调用_get_fixturenames_closure_and_arg2fixturedefs,并更新结果。
- 如果
-
返回空结果:
- 如果
value不是上述任何类型,返回空列表和空字典作为默认结果。
- 如果
_copy_metafunc方法
这个函数的作用是创建一个与传入的 metafunc 对象相似但某些属性被重新初始化或复制的新对象。这种复制操作在一些需要对对象进行修改但又不想影响原始对象的场景中可能会用到。
逐步解释:
-
copied = copy.copy(metafunc):使用copy.copy函数创建了metafunc的浅复制,并将其存储在copied变量中。 -
copied.fixturenames = copy.copy(metafunc.fixturenames):复制了metafunc对象的fixturenames属性。 -
copied._calls = []:将copied对象的_calls属性初始化为一个空列表。 -
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs):复制了metafunc对象的_arg2fixturedefs属性。 -
最后,函数返回复制后的
copied对象。
_normalize_call方法
_normalize_call 函数用于处理 pytest 的调用规范(callspec)和元数据(metafunc)。
代码逻辑如下:
-
函数的主要目的是处理
callspec中的参数,特别是那些可能是延迟加载fixtures的参数。 -
它通过
_get_fixturenames_closure_and_arg2fixturedefs函数来提取fixtures信息,并根据这些信息创建新的测试用例。 -
如果参数值包含延迟加载的
fixtures,会创建metafunc的副本并更新其属性,然后生成新的测试。 -
函数递归地处理
metafunc,以确保所有相关的fixtures都被正确处理。
-
函数定义:
def _normalize_call(callspec, metafunc, used_keys): 定义了函数,接受三个参数:callspec(调用规范),metafunc(元数据函数),used_keys(已使用的关键字参数集合)。
-
获取 funcmanage 插件:
fm = metafunc.config.pluginmanager.get_plugin("funcmanage"): 从pytest的插件管理器中获取funcmanage插件的实例。
-
初始化 used_keys:
used_keys = used_keys or set(): 如果used_keys未提供或为None,则初始化为一个新的空集合。
-
复制 params:
params = callspec.params.copy(): 复制callspec中的params字典。对于pytest 8.0.0及以上版本,params已经包含了所有参数;对于更低版本,还需要合并callspec.funcargs。
-
计算 valtype_keys:
valtype_keys = params.keys() - used_keys: 找出params中尚未使用的关键字参数键。
-
遍历 valtype_keys:
- 遍历
valtype_keys中的每个参数名arg。
- 遍历
-
提取 fixtures 信息:
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value): 对于每个参数值value,调用_get_fixturenames_closure_and_arg2fixturedefs函数来获取fixtures名称闭包和详细信息。
-
处理额外的 fixtures:
- 如果
fixturenames_closure和arg2fixturedefs都不为空,表示找到了额外的fixtures:- 从
fixturenames_closure中筛选出尚未在params中的fixtures名称。 - 创建
metafunc的副本newmetafunc。 - 更新
newmetafunc的fixturenames和_arg2fixturedefs。 - 设置
newmetafunc._calls为当前的callspec。 - 调用
fm.pytest_generate_tests(newmetafunc)来生成新的测试。 - 递归调用
normalize_metafunc_calls来进一步处理newmetafunc。 - 返回
newmetafunc._calls。
- 从
- 如果
-
更新 used_keys:
used_keys.add(arg): 将当前处理的参数名arg添加到used_keys集合中。
-
返回 callspec:
- 如果没有额外的
fixtures需要处理,或者在处理过程中没有找到新的fixtures,返回原始的callspec列表。
- 如果没有额外的
normalize_metafunc_calls方法
normalize_metafunc_calls函数用于处理 pytest 的元数据函数(metafunc)和调用规范(callspec)。
代码逻辑如下:
-
函数的目的是规范化
metafunc中的调用规范,特别是处理延迟加载的fixtures。 -
它通过
_normalize_call函数来处理每个callspec,这可能会生成新的调用规范,特别是当参数值包含延迟加载的fixtures时。 -
处理后的调用规范被收集到
newcalls列表中,并最终替换metafunc的原始_calls列表。
详细解释如下:
-
函数定义:
def normalize_metafunc_calls(metafunc, used_keys=None): 定义了函数,接受两个参数:metafunc: 一个pytest测试函数的元数据对象,包含有关测试函数的信息,如参数和fixtures。used_keys: 一个可选参数,表示已经使用的关键字参数的集合。如果没有提供,它将被初始化为None。
-
初始化新调用列表:
newcalls = []: 创建一个新的空列表newcalls,用于存储处理后的调用规范。
-
遍历原始调用规范:
for callspec in metafunc._calls: 遍历metafunc中的_calls属性,这是一个包含所有调用规范的列表。
-
处理每个调用规范:
calls = _normalize_call(callspec, metafunc, used_keys): 对每个callspec调用_normalize_call函数,传入当前的callspec、metafunc和used_keys。这将处理延迟加载的 fixtures 并生成新的调用规范。
-
扩展新调用列表:
newcalls.extend(calls): 将_normalize_call函数返回的新调用规范列表扩展到newcalls列表中。
-
更新元数据函数的调用规范:
metafunc._calls = newcalls: 用处理后的newcalls列表更新metafunc的_calls属性。
normalizer.py文件
import copy
from typing import Any, Dict, List, Tuple
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .lazy_fixture_callable import LazyFixtureCallableWrapper
def _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value) -> Tuple[List[str], Dict[str, Any]]:
if isinstance(value, LazyFixtureCallableWrapper):
extra_fixturenames_args, arg2fixturedefs_args = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.args,
)
extra_fixturenames_kwargs, arg2fixturedefs_kwargs = _get_fixturenames_closure_and_arg2fixturedefs(
fm,
metafunc,
value.kwargs,
)
return [*extra_fixturenames_args, *extra_fixturenames_kwargs], {
**arg2fixturedefs_args,
**arg2fixturedefs_kwargs,
}
if isinstance(value, LazyFixtureWrapper):
if pytest.version_tuple >= (8, 0, 0):
fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure(metafunc.definition.parent, [value.name], {})
else: # pragma: no cover
# TODO: add tox
_, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([value.name], metafunc.definition.parent)
return fixturenames_closure, arg2fixturedefs
extra_fixturenames, arg2fixturedefs = [], {}
# we need to check exact type
if type(value) is dict: # noqa: E721
value = list(value.values())
# we need to check exact type
if type(value) in {list, tuple, set}:
for val in value:
ef, arg2f = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, val)
extra_fixturenames.extend(ef)
arg2fixturedefs.update(arg2f)
return extra_fixturenames, arg2fixturedefs
def normalize_metafunc_calls(metafunc, used_keys=None):
newcalls = []
for callspec in metafunc._calls:
calls = _normalize_call(callspec, metafunc, used_keys)
newcalls.extend(calls)
metafunc._calls = newcalls
def _copy_metafunc(metafunc):
copied = copy.copy(metafunc)
copied.fixturenames = copy.copy(metafunc.fixturenames)
copied._calls = []
copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
return copied
def _normalize_call(callspec, metafunc, used_keys):
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
used_keys = used_keys or set()
params = callspec.params.copy() if pytest.version_tuple >= (8, 0, 0) else {**callspec.params, **callspec.funcargs}
valtype_keys = params.keys() - used_keys
for arg in valtype_keys:
value = params[arg]
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
if fixturenames_closure and arg2fixturedefs:
extra_fixturenames = [fname for fname in fixturenames_closure if fname not in params]
newmetafunc = _copy_metafunc(metafunc)
newmetafunc.fixturenames = extra_fixturenames
newmetafunc._arg2fixturedefs.update(arg2fixturedefs)
newmetafunc._calls = [callspec]
fm.pytest_generate_tests(newmetafunc)
normalize_metafunc_calls(newmetafunc, used_keys | {arg})
return newmetafunc._calls
used_keys.add(arg)
return [callspec]
对代解析如下:
导入依赖
-
import copy:导入Python标准库中的copy模块,用于复制对象。 -
from typing import Any, Dict, List, Tuple:导入类型注解,用于函数返回类型和参数类型。 -
import pytest:导入pytest模块,用于与pytest框架集成。 -
from .lazy_fixture import LazyFixtureWrapper和from .lazy_fixture_callable import LazyFixtureCallableWrapper:从同一包中导入LazyFixtureWrapper和LazyFixtureCallableWrapper类。
_get_fixturenames_closure_and_arg2fixturedefs 函数
-
功能:获取与延迟加载的
fixture相关的fixtures名称闭包和fixtures定义。 -
参数:
fm:pytest的funcmanage插件实例。metafunc:代表当前测试函数的元数据对象。value:可能是延迟加载的fixture或其他值。
-
返回值:一个元组,包含
fixtures名称的列表和fixtures定义的字典。
normalize_metafunc_calls 函数
-
功能:规范化
metafunc中的调用规范,处理延迟加载的fixtures。 -
参数:
metafunc:pytest测试函数的元数据对象。used_keys:一个可选参数,表示已经使用的关键字参数的集合。
-
逻辑:遍历
metafunc._calls中的每个callspec,调用_normalize_call函数处理,并更新metafunc._calls。
_copy_metafunc 函数
-
功能:复制
metafunc对象,创建一个新的元数据对象。 -
参数:
metafunc。 -
返回值:一个新的
metafunc副本。
_normalize_call 函数
-
功能:处理单个调用规范,提取延迟加载的
fixtures并生成新的调用规范。 -
参数:
callspec:当前的调用规范。metafunc:pytest测试函数的元数据对象。used_keys:表示已经使用的关键字参数的集合。
-
逻辑:
- 获取
funcmanage插件实例。 - 复制
params字典,并计算未使用的关键字参数键(valtype_keys)。 - 遍历
valtype_keys,对于每个参数值value,调用_get_fixturenames_closure_and_arg2fixturedefs函数获取fixtures名称闭包和定义。 - 如果获取到额外的
fixtures名称和定义,创建metafunc的副本,更新其fixturenames和_arg2fixturedefs,生成新的调用规范并返回。 - 如果没有额外的
fixtures,将当前的callspec添加到used_keys并返回。
- 获取
代码逻辑总结
-
这段代码通过递归和类型检查,确保所有可能包含延迟加载
fixtures的参数都被检查并处理。 -
_get_fixturenames_closure_and_arg2fixturedefs函数是核心,它负责提取fixtures信息。 -
normalize_metafunc_calls和_normalize_call函数处理调用规范,生成新的测试用例。 -
_copy_metafunc函数用于创建metafunc的副本,以便在生成新测试时保持原始metafunc不变。
plugin.py文件
import pytest
from .lazy_fixture import LazyFixtureWrapper
from .loader import load_lazy_fixtures
from .normalizer import normalize_metafunc_calls
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(fixturedef, request):
val = getattr(request, "param", None)
if val is not None:
request.param = load_lazy_fixtures(val, request)
def pytest_make_parametrize_id(config, val, argname):
if isinstance(val, LazyFixtureWrapper):
return val.name
@pytest.hookimpl(hookwrapper=True)
def pytest_generate_tests(metafunc):
yield
normalize_metafunc_calls(metafunc)
代码详细解析:
导入依赖
-
import pytest:导入pytest模块,用于与pytest框架集成。 -
from .lazy_fixture import LazyFixtureWrapper:从同一包中导入LazyFixtureWrapper类,用于包装延迟加载的fixture。 -
from .loader import load_lazy_fixtures:从loader模块导入load_lazy_fixtures函数,用于加载延迟加载的fixtures。 -
from .normalizer import normalize_metafunc_calls:从normalizer模块导入normalize_metafunc_calls函数,用于规范化metafunc中的调用规范。
pytest_fixture_setup 钩子函数
-
@pytest.hookimpl(tryfirst=True):装饰器,将函数注册为pytest的钩子,tryfirst=True表示尽可能早地执行此钩子。 -
功能:在 fixture 设置阶段被调用,用于处理延迟加载的
fixtures。 -
参数:
fixturedef:fixture的定义。request:包含测试函数和fixture请求的上下文。
-
逻辑:
- 获取
request的param属性,如果存在,则使用load_lazy_fixtures函数加载延迟加载的fixtures,并将结果重新赋值给request.param。
- 获取
pytest_make_parametrize_id 钩子函数
-
功能:用于生成参数化测试的 ID。
-
参数:
config:pytest的配置对象。val:参数值。argname:参数名称。
-
逻辑:
- 如果
val是LazyFixtureWrapper的实例,返回其名称name作为参数化测试的ID。
- 如果
pytest_generate_tests 钩子函数
-
@pytest.hookimpl(hookwrapper=True):装饰器,将函数注册为pytest的钩子,hookwrapper=True表示此钩子会包装其他钩子的执行。 -
功能:在生成测试用例时被调用,用于处理延迟加载的
fixtures。 -
参数:
metafunc:代表当前测试函数的元数据对象。
-
逻辑:
- 使用
yield挂起函数执行,等待其他钩子执行完毕后再继续。 - 调用
normalize_metafunc_calls函数处理metafunc,确保所有延迟加载的fixtures被正确处理。
- 使用
代码逻辑总结
-
这段代码通过
pytest的钩子机制,实现了对延迟加载fixtures的支持。 -
pytest_fixture_setup钩子在fixture设置阶段加载延迟加载的fixtures,并更新request.param。 -
pytest_make_parametrize_id钩子为参数化测试生成 ID,特别处理LazyFixtureWrapper实例。 -
pytest_generate_tests钩子在生成测试用例时规范化metafunc,确保所有延迟加载的fixtures被正确处理。
结语
pytest_lazy_fixtures 是一个 pytest 插件,它允许你以惰性方式加载 fixtures,即直到真正需要它们时才进行加载。这可以减少测试的初始化时间,并降低资源消耗,特别是在处理复杂的或耗时的 fixture 时非常有用。
-
编写测试:在你的测试模块中,使用
lf或lfc函数来引用fixtures,而不是直接使用pytest.fixture装饰器。 -
使用 LazyFixtureWrapper:
lf函数返回一个LazyFixtureWrapper对象,它包装了fixture的名称,并在运行时才加载它。 -
使用 LazyFixtureCallableWrapper:
lfc函数允许你延迟调用fixtures,并且可以传递参数和关键字参数。 -
运行测试:使用
pytest运行测试,插件会自动处理延迟加载的fixtures。 -
配置 pytest:如果需要,你可以在
conftest.py或测试模块中配置插件,例如设置默认的fixtures。
通过使用 pytest_lazy_fixtures 插件,你可以使测试套件更加高效,特别是在处理多个或复杂的 fixtures 时。它提供了一种灵活的方式来组织和执行测试,同时保持了测试的清晰性和可维护性。