Pytest的断言失败输出比Python中的默认输出更具信息性和有用性。我想在正常运行Python程序时利用此功能,而不仅仅是在执行测试时使用。有没有办法在我的脚本中覆盖Python的assert
行为,以便在仍以python script/pytest_assert.py
运行程序的同时使用pytest打印堆栈跟踪信息?
def test_foo():
foo = 12
bar = 42
assert foo == bar
if __name__ == '__main__':
test_foo()
$ python script/pytest_assert.py
Traceback (most recent call last):
File "script/pytest_assert.py", line 8, in <module>
test_foo()
File "script/pytest_assert.py", line 4, in test_foo
assert foo == bar
AssertionError
$ pytest script/pytest_assert.py
======================== test session starts ========================
platform linux -- Python 3.5.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: /usr/local/google/home/danijar, inifile:
collected 1 item
script/pytest_assert.py F [100%]
============================= FAILURES ==============================
_____________________________ test_foo ______________________________
def test_foo():
foo = 12
bar = 42
> assert foo == bar
E assert 12 == 42
script/pytest_assert.py:4: AssertionError
===================== 1 failed in 0.02 seconds =====================
$ python script/pytest_assert.py
Traceback (most recent call last):
File "script/pytest_assert.py", line 8, in <module>
test_foo()
def test_foo():
foo = 12
bar = 42
> assert foo == bar
E assert 12 == 42
script/pytest_assert.py:4: AssertionError
我最接近的是这个,但它仅适用于该函数中的断言并向跟踪发送垃圾邮件:
import ast
import inspect
from _pytest import assertion
def test_foo():
foo = []
foo.append(13)
foo = foo[-1]
bar = 42
assert foo == bar, 'message'
if __name__ == '__main__':
tree = ast.parse(inspect.getsource(test_foo))
assertion.rewrite.rewrite_asserts(tree)
code = compile(tree, '<name>', 'exec')
ns = {}
exec(code, ns)
ns[test_foo.__name__]()
$ python script/pytest_assert.py
Traceback (most recent call last):
File "script/pytest_assert.py", line 21, in <module>
ns[test_foo.__name__]()
File "<name>", line 6, in test_foo
AssertionError: message
assert 13 == 42
答案 0 :(得分:2)
尽管肯定有一种方法可以重用pytest
代码以所需格式打印回溯,但是您需要使用的东西不是公共API的一部分,因此生成的解决方案将太脆弱,需要调用不相关的pytest
代码(出于初始化目的),可能会导致软件包更新中断。最好的选择是重写pytest
代码作为示例。
基本上,下面的概念验证代码可以完成三件事:
将默认的sys.excepthook
替换为自定义的ExceptionInfo.getrepr()
:这是更改默认回溯格式的必要条件。示例:
import sys
orig_hook = sys.excepthook
def myhook(*args):
orig_hook(*args)
print('hello world')
if __name__ == '__main__':
sys.excepthook = myhook
raise ValueError()
将输出:
Traceback (most recent call last):
File "example.py", line 11, in <module>
raise ValueError()
ValueError
hello world
将打印格式化的异常信息,而不是hello world
。为此,我们使用this old article。
要访问断言中的其他信息,pytest
会重写assert
语句(您可以在PEP 302中重写后获得一些粗略的信息)。为此,pytest
注册了Config
中指定的自定义导入挂钩。挂钩是最有问题的部分,因为它与AssertionRewriter
对象紧密耦合,我也注意到一些模块导入会引起问题(我猜想pytest
不会失败,仅是因为模块已经被导入了)当挂钩被注册时;将尝试编写一个测试,该测试在pytest
运行时重现该问题并创建一个新的问题)。因此,我建议编写一个调用{{3}}的自定义导入挂钩。这个AST树漫游者类是断言重写中必不可少的部分,而AssertionRewritingHook
并不那么重要。
so-51839452
├── hooks.py
├── main.py
└── pytest_assert.py
hooks.py
import sys
from pluggy import PluginManager
import _pytest.assertion.rewrite
from _pytest._code.code import ExceptionInfo
from _pytest.config import Config, PytestPluginManager
orig_excepthook = sys.excepthook
def _custom_excepthook(type, value, tb):
orig_excepthook(type, value, tb) # this is the original traceback printed
# preparations for creation of pytest's exception info
tb = tb.tb_next # Skip *this* frame
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
info = ExceptionInfo(tup=(type, value, tb, ))
# some of these params are configurable via pytest.ini
# different params combination generates different output
# e.g. style can be one of long|short|no|native
params = {'funcargs': True, 'abspath': False, 'showlocals': False,
'style': 'long', 'tbfilter': False, 'truncate_locals': True}
print('------------------------------------')
print(info.getrepr(**params)) # this is the exception info formatted
del type, value, tb # get rid of these in this frame
def _install_excepthook():
sys.excepthook = _custom_excepthook
def _install_pytest_assertion_rewrite():
# create minimal config stub so AssertionRewritingHook is happy
pluginmanager = PytestPluginManager()
config = Config(pluginmanager)
config._parser._inidict['python_files'] = ('', '', [''])
config._inicache = {'python_files': None, 'python_functions': None}
config.inicfg = {}
# these modules _have_ to be imported, or AssertionRewritingHook will complain
import py._builtin
import py._path.local
import py._io.saferepr
# call hook registration
_pytest.assertion.install_importhook(config)
# convenience function
def install_hooks():
_install_excepthook()
_install_pytest_assertion_rewrite()
main.py
调用hooks.install_hooks()
之后,main.py
将修改回溯打印。在install_hooks()
调用之后导入的每个模块都将在导入时重写断言。
from hooks import install_hooks
install_hooks()
import pytest_assert
if __name__ == '__main__':
pytest_assert.test_foo()
pytest_assert.py
def test_foo():
foo = 12
bar = 42
assert foo == bar
$ python main.py
Traceback (most recent call last):
File "main.py", line 9, in <module>
pytest_assert.test_foo()
File "/Users/hoefling/projects/private/stackoverflow/so-51839452/pytest_assert.py", line 4, in test_foo
assert foo == bar
AssertionError
------------------------------------
def test_foo():
foo = 12
bar = 42
> assert foo == bar
E AssertionError
pytest_assert.py:4: AssertionError
我会写自己的AssertionRewritingHook
版本,而没有整个无关的pytest
东西。 AssertionRewriter
看起来非常可重用;尽管它需要一个Config
实例,但是它仅用于警告打印,可以保留给None
。
有了这些,编写自己的函数以正确格式化异常,替换sys.excepthook
,就可以完成。