pytest学习

Posted by Shi Hai's Blog on July 17, 2023

基础使用

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并进行计算,代码详情

参考文献