基础使用
TBD
插件管理
插件管理核心机制
插件的核心管理机制是pluggy
,通过pluggy
对插件进行注册和管理。pytest重新扩展了一个PluginManager
来管理所有插件,初始化过程可以看这里。
插件类别
插件类别主要有三类:
- 内置插件(builtin plugins):从pytest项目的_pytest目录中加载,pytest的内置插件清单还可以看这里;
- 外部插件(external plugins):从setuptools配置入口进行加载;
- conftest.py插件:从测试目录中自动识别及加载。
所有的插件定义和实现都要以
pytest_
前缀实现,方便插件的快速被加载。
插件加载
用conftest.py文件定义pytest_plugins属性就可以让pytest在执行用例之前加载相关插件,参考文档。对应的实际处理逻辑是:pytest的PytestPluginManager
定义了一个import_plugin
可以用来加载插件。pytest_plugins
的加载实际动作在PytestPluginManager.consider_module()
中执行,代码实现逻辑。
hook函数
pytest从有hook函数定义的注册插件中调用hook函数。内置插件的hook函数定义在hookspec.py文件中。
fixture
fixture可以用于初始化测试函数,它们提供固定的基线,以便测试可靠地执行并产生一致且可重复的结果。初始化可以设置服务、状态或其他操作环境。
FixtureFunctionMarker
我们可以使用@pytest.fixture()
装饰器来创建一个fixture。这个装饰器里面的最核心类是FixtureFunctionMarker
。
我们只能通过参数入参的方式来调用fixture,而无法通过函数直接调用否则会报此类错误,代码详情:
Failed: Fixture "fixture_name" called directly. Fixtures are not meant to be called directly
Function
class Function
是对一个完整测试函数的封装,所有的准备及执行动作都在这个类实例中完成,如:fixtures的填充过程就是在setup()
函数中执行完成的。
class Function(PyobjMixin, nodes.Item):
"""Item responsible for setting up and executing a Python test function."""
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = fixtures.TopRequest(self, _ispytest=True)
@property
def _pyfuncitem(self):
"""(compatonly) for code expecting pytest-2.2 style request objects."""
return self
def runtest(self) -> None:
"""Execute the underlying test function."""
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self) -> None:
self._request._fillfixtures()
...
Session
Function
对象实例是在Session
实例化过程中生成了所有的Function
对象,这个执行动作是由main.pytest_collection()
函数触发,而真正的遍历及生成item
/Function
实例对象的函数是Session.genitems()
,比如我要执行一个test.py文件,则会触发session.genitems("test.py")
并创建出所有的Function对象实例。
class Session(nodes.FSCollector):
"""The root of the collection tree.
``Session`` collects the initial paths given as arguments to pytest.
"""
def genitems(
self, node: Union[nodes.Item, nodes.Collector]
) -> Iterator[nodes.Item]:
self.trace("genitems", node)
if isinstance(node, nodes.Item):
node.ihook.pytest_itemcollected(item=node)
yield node
else:
assert isinstance(node, nodes.Collector)
rep = collect_one_node(node)
if rep.passed:
for subnode in rep.result:
yield from self.genitems(subnode)
node.ihook.pytest_collectreport(report=rep)
Module
上面Session.genitems()
函数实际调用执行的核心逻辑是Module.collect()
来进行fixture信息的遍历索引。在Module.collect()
函数中的self.session._fixturemanager.parsefactories(self)
就会遍历Module对象然后生成对应所有的FixtureDef
实例对象。
class Module(nodes.File, PyCollector):
"""Collector for test classes and functions in a Python module."""
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
XXRequest
实际需要调用的Fixture相关信息(涉及到的FixtureDef
都在Function._fixtureinfo
中)都在Function
对象实例中,并在构造TopReuqest
对象实例时会将Function
对象实例注入其中。
class TopRequest(FixtureRequest):
"""The type of the ``request`` fixture in a test function."""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
super().__init__(
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
...
class FixtureRequest(abc.ABC):
"""The type of the ``request`` fixture."""
@property
def fixturenames(self) -> List[str]:
"""Names of all active fixtures in this request."""
result = list(self._pyfuncitem._fixtureinfo.names_closure)
result.extend(set(self._fixture_defs).difference(result))
return result
def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
fixturedef = self._fixture_defs.get(argname)
if fixturedef is None:
try:
fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)
raise
self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef
return fixturedef
def getfixturevalue(self, argname: str) -> Any:
"""Dynamically run a named fixture function."""
fixturedef = self._get_active_fixturedef(argname)
assert fixturedef.cached_result is not None, (
f'The fixture value for "{argname}" is not available. '
"This can happen when the fixture has already been torn down."
)
return fixturedef.cached_result[0]
def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
"""Create a SubRequest based on "self" and call the execute method
of the given FixtureDef object.
This will force the FixtureDef object to throw away any previous
results and compute a new fixture value, which will be stored into
the FixtureDef object itself.
"""
...
...
在上面的FixtureRequest.getfixturevalue()
函数中有一个逻辑是返回fixturedef.cached_result
中的结果,实际这个执行逻辑在pytest_fixture_setup()
函数中:
def pytest_fixture_setup(
fixturedef: FixtureDef[FixtureValue], request: SubRequest
) -> FixtureValue:
"""Execution of fixture setup."""
kwargs = {}
for argname in fixturedef.argnames:
fixdef = request._get_active_fixturedef(argname)
assert fixdef.cached_result is not None
result, arg_cache_key, exc = fixdef.cached_result
request._check_scope(argname, request._scope, fixdef._scope)
kwargs[argname] = result
fixturefunc = resolve_fixture_function(fixturedef, request)
my_cache_key = fixturedef.cache_key(request)
try:
result = call_fixture_func(fixturefunc, request, kwargs)
except TEST_OUTCOME as e:
if isinstance(e, skip.Exception):
# The test requested a fixture which caused a skip.
# Don't show the fixture as the skip location, as then the user
# wouldn't know which test skipped.
e._use_item_location = True
fixturedef.cached_result = (None, my_cache_key, e)
raise
fixturedef.cached_result = (result, my_cache_key, None)
return result
FixtureDef
在执行测试中,测试函数通过执行fixturedef = request._get_active_fixturedef(argname)
来查询入参对应的fixutre并进行计算,代码详情。