如何在 PyTest 中调用时立即访问测试的堆栈帧?

问题描述

我正在编写一个 PyTest 插件,我想在调用时立即访问每个测试的堆栈帧。具体来说,我想访问每个测试的全局和本地命名空间,以便我可以通过名称查看它有权访问的对象。

目前,我使用配置文件函数来实现这一点,设置为 sys.setprofile,它检查框架正在执行的代码对象的名称是否与将要执行的测试函数名称匹配,以及事件为 "call",表示刚刚调用了测试。

我发现这种方法会引入不必要的时间开销,甚至会干扰某些测试套件的执行,所以我想知道是否有更简洁有效的方法

解决方法

您可以在 pytest_pyfunc_call 周围使用一个钩子包装器,这是最接近实际调用测试函数的钩子,以外科手术应用(然后删除)具有非常具体的身份过滤器的 profilefunc。

# conftest.py
import sys

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    testfunction = pyfuncitem.obj
    testfunction_code = testfunction.__code__

    def profilefunc(frame,event,arg):
        if frame.f_code is testfunction_code and event == 'call':
            ctx_globals = frame.f_globals
            ctx_locals = frame.f_locals

            print('\nglobals:',tuple(ctx_globals),'\nlocals:',ctx_locals,'\n')

            sys.setprofile(None)

    sys.setprofile(profilefunc)
    try:
        outcome = yield
        outcome.get_result()
    finally:
        sys.setprofile(None)

因为 pytest_pyfunc_call 可以访问将被调用的实际测试方法对象 - 连同其代码对象 - 并且传递给 profilefunc 的 frame 参数包含对正在执行的代码对象的引用,我们可以使用快速身份比较(is 运算符)来限制我们的工作。

使用这种方法,profilefunc 的调用次数将比一般应用时少得多(并且为每次调用调用),并且不会产生每次调用的字符串比较成本。

针对此测试测试套件运行时

class DescribeIt:
    def it_works(self,request):
        a = 12
        b = 24
        assert True

我们看到我们的全球名称和本地名称被打印出来

$ py.test -s --no-header --no-summary -q

tests/test_client.py::DescribeIt::it_works 

globals: ('__name__','__doc__','__package__','__loader__','__spec__','__file__','__cached__','__builtins__','@py_builtins','@pytest_ar','DescribeIt') 
locals: {'self': <tests.test_client.DescribeIt object at 0x7fc5b6d6ee80>,'request': <FixtureRequest for <Function it_works>>} 

在这个测试中,我们的 profilefunc 被调用了 58 次。

如果我们希望更多手术,我们可以及时变异 pyfuncitem.obj 以将我们的 profilefunc 调用减少到 1!

# conftest.py
import sys

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    testfunction = pyfuncitem.obj
    testfunction_code = testfunction.__code__

    def profilefunc(frame,arg):
        if frame.f_code is testfunction_code and event == 'call':
            sys.setprofile(None)

            ctx_globals = frame.f_globals
            ctx_locals = frame.f_locals

            print('\nglobals:','\n')

    def profiled_testfunction(*args,**kwargs):
        sys.setprofile(profilefunc)
        try:
            return testfunction(*args,**kwargs)
        finally:
            sys.setprofile(None)

    pyfuncitem.obj = profiled_testfunction
    try:
        outcome = yield
        outcome.get_result()
    finally:
        pyfuncitem.obj = testfunction

请注意,在所有这些中,在为我们的测试方法调用时以及在 sys.setprofile(None) 子句中调用 finally 以删除 profilefunc。这是一种预防措施,以防错误或其他奇怪的情况导致我们的测试方法调用 profilefunc,并且我们冒着保持 profilefunc 处于活动状态的风险。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...