问题描述
我正在编写一个 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 处于活动状态的风险。