您正在使用pytest
,这为您提供了与失败的测试进行交互的充足选项。它为您提供了命令行选项和一些挂钩,以实现此目的。我将说明如何使用每种方法以及在何处可以进行自定义以满足您特定的调试需求。
如果确实需要,我还将介绍更多奇特的选项,这些选项将允许您完全跳过特定的断言。
处理异常,而不是断言
请注意,失败的测试通常不会停止pytest;仅当您启用显式告诉它在一定数量的失败后退出时,才可以。同样,测试失败是因为引发了异常。assert
引发,AssertionError
但这不是唯一会导致测试失败的异常!您想控制异常的处理方式,而不是alter assert
。
但是,失败的断言将终止单个测试。这是因为一旦在try...except
块外引发了异常,Python就会展开当前函数框架,并且没有任何回溯了。
从您对_assertCustom()
尝试重新运行该断言的描述来看,我认为这不是您想要的,但是尽管如此,我将进一步讨论您的选择。
使用PDB在pytest中进行事后调试
对于处理调试器中的故障的各种选项,我将--pdb
从命令行开关开始,该开关在测试失败(为简洁起见而省略输出)时打开标准调试提示:
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
使用此开关,当测试失败时,pytest启动事后调试会话。本质上,这正是您想要的。在测试失败时停止代码并打开调试器以查看测试状态。您可以与测试的局部变量,全局变量以及堆栈中每个框架的局部变量和全局变量进行交互。
在这里pytest可以让您完全控制是否在此之后退出:如果使用q
quit命令,则pytest也会退出运行,使用c
for continue将控制权返回pytest并执行下一个测试。
使用替代调试器
您不必为此受pdb
调试器的约束;您可以使用--pdbcls
开关设置其他调试器。任何pdb.Pdb()
兼容的实现都可以使用,包括IPython调试器实现或大多数其他Python调试器(pudb调试器要求使用该-s
开关或特殊的插件)。开关带有一个模块和类,例如要使用,pudb
您可以使用:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
您可以使用此功能来编写自己的包装类各地Pdb
,简单地立即返回,如果特定的故障是不是你感兴趣的pytest
用途Pdb()
一样准确pdb.post_mortem()
呢:
p = Pdb()
p.reset()
p.interaction(None, t)
这里t
是一个回溯对象。当p.interaction(None, t)
返回时,pytest
继续进行下一个测试,除非 p.quitting
被设置为True
(在该点处pytest然后退出)。
这是一个示例实现,可以打印出我们拒绝调试并立即返回的结果,除非将test提升为ValueError
,另存为demo/custom_pdb.py
:
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
当我在上面的演示中使用它时,这是输出(同样,为简洁起见,省略了它):
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
上面的内省sys.last_type
确定了故障是否“有趣”。
但是,除非您想使用tkInter或类似的东西编写自己的调试器,否则我不能真正推荐此选项。请注意,这是一项艰巨的任务。
过滤失败;选择并选择何时打开调试器
下一阶段是pytest 调试和交互挂钩;这些是行为自定义的挂钩点,以替换或增强pytest通常如何处理诸如处理异常或通过pdb.set_trace()
或breakpoint()
(Python 3.7或更高版本)进入调试器的方式。
此挂钩的内部实现也负责打印>>> entering PDB >>>
上方的横幅,因此使用此挂钩来阻止调试器运行意味着您根本看不到此输出。您可以拥有自己的钩子,然后在测试失败“很有趣”时委托给原始钩子,因此可以独立于所使用的调试器来过滤测试失败!您可以通过按名称访问内部实现来访问它;内部挂钩插件的名称为pdbinvoke
。为了防止它运行,您需要注销它,但要保存引用,我们可以根据需要直接调用它。
这是这种钩子的示例实现;您可以将其放在插件从中加载的任何位置;我把它放在demo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
上面的插件使用内部TerminalReporter
插件向终端写线;使用默认的紧凑测试状态格式时,这可以使输出更整洁,并且即使启用了输出捕获,也可以将内容写入终端。
该示例pytest_exception_interact
通过另一个钩子向钩子注册了插件对象pytest_configure()
,但要确保它运行得足够晚(使用@pytest.hookimpl(trylast=True)
),以能够取消注册内部pdbinvoke
插件。调用该钩子时,该示例将针对该call.exceptinfo
对象进行测试;您也可以检查节点或报告。
与代替上述示例代码中demo/conftest.py
,该test_ham
测试失败,则忽略,仅test_spam
测试失败,这引起了ValueError
,结果在调试提示开口:
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
重申一下,上述方法的另一个优点是,您可以将其与任何与pytest一起使用的调试器(包括pudb或IPython调试器)结合使用:
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
它还有关于运行什么测试(通过node
参数)和直接访问引发的异常(通过call.excinfo
ExceptionInfo
实例)的更多上下文。
请注意,特定的pytest调试器插件(例如pytest-pudb
或pytest-pycharm
)注册了自己的pytest_exception_interact
hooksp。一个更完整的实现将必须遍历plugin-manager中的所有插件,以自动覆盖,使用config.pluginmanager.list_name_plugin
和hasattr()
测试每个插件的任意插件。
使失败彻底消失
尽管这可以完全控制失败的测试调试,但是即使您选择不为给定测试打开调试器,这仍然会使测试失败。如果您想使故障完全消失,则可以使用其他钩子:pytest_runtest_call()
。
pytest运行测试时,它将通过上面的钩子运行测试,该钩子将返回None
或引发异常。由此创建一个报告,可以选择创建一个日志条目,如果测试失败,pytest_exception_interact()
则调用上述钩子。因此,您所需要做的就是更改此挂钩产生的结果。除了异常之外,它根本不返回任何内容。
最好的方法是使用钩子包装。挂钩包装器不必执行实际工作,而是有机会更改挂钩结果。您所要做的就是添加以下行:
outcome = yield
在挂钩包装器实现中,您可以通过访问挂钩结果,包括测试异常outcome.excinfo
。如果测试中引发异常,则此属性设置为(类型,实例,回溯)的元组。或者,您可以调用outcome.get_result()
并使用标准try...except
处理。
那么,如何通过不合格的测试呢?您有3个基本选项:
您使用什么取决于您。请确保首先检查跳过和预期失败测试的结果,因为您无需像测试失败那样处理这些情况。您可以通过pytest.skip.Exception
和访问这些选项引发的特殊异常pytest.xfail.Exception
。
这是一个示例实现,将未引发的失败测试标记ValueError
为跳过:
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
输入后conftest.py
,输出将变为:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
我使用该-r a
标志使它变得更清晰,test_ham
现在已被跳过。
如果更换pytest.skip()
电话用pytest.xfail("[XFAIL] ignoring everything but ValueError")
,测试被标记为预期故障:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
并使用将其outcome.force_result([])
标记为已通过:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
由您决定最适合您的用例的是您。对于skip()
和xfail()
我模仿了标准消息格式(以[NOTRUN]
或前缀[XFAIL]
),但是您可以自由使用所需的任何其他消息格式。
在这三种情况下,pytest都不会为您使用此方法更改了结果的测试打开调试器。
更改单个断言语句
如果你想改变assert
测试的测试范围内,那么你是在和自己的一大堆更多的工作。是的,这在技术上是可行的,但是只能通过重写Python将在编译时执行的代码来实现。
使用时pytest
,实际上已经完成了。当你的断言失败时, Pytest 重写assert
语句给你更多的上下文 ; 请参阅此博客文章,以详细了解正在执行的工作以及_pytest/assertion/rewrite.py
源代码。请注意,该模块的长度超过1k行,并且要求您了解Python的抽象语法树的工作方式。如果这样做,您可以对该模块进行猴子修补,以在其中添加您自己的修改,包括assert
使用try...except AssertionError:
处理程序将其包围。
但是,您不能只是选择性地禁用或忽略断言,因为后续语句很容易取决于跳过的断言旨在防止的状态(特定的对象排列,变量集等)。如果一个断言测试foo
不是None
,那么后面的一个断言依赖于foo.bar
存在,那么您只会遇到一个断言,以此类推。AttributeError
如果您需要遵循此路线,请坚持重新引发异常。
我不打算在asserts
这里更详细地进行重写,因为我认为这不值得追求,没有考虑到所涉及的工作量,并且事后调试使您可以访问测试的状态。断言点仍然失败。
请注意,如果您确实想执行此操作,则无需使用eval()
(无论如何它assert
都是行不通的,它是一条语句,因此您需要使用它exec()
),也不必运行两次断言(如果在声明中使用的表达式更改了状态,可能会导致问题。相反,您可以将ast.Assert
节点嵌入节点内ast.Try
,并附加一个使用空ast.Raise
节点的except处理程序,以重新引发捕获的异常。
使用调试器跳过断言语句。
Python调试器实际上允许您使用/ 命令跳过语句。如果你知道了前面一个特定的断言会失败,你可以用它来绕过它。您可以使用来运行测试,该测试会在每次测试开始时打开调试器,然后在调试器在断言之前暂停时发出a 来跳过调试器。j
jump
--trace
j <line after assert>
您甚至可以自动执行此操作。使用以上技术,您可以构建一个自定义调试器插件,该插件
- 使用
pytest_testrun_call()
钩子捕获AssertionError
异常
- 从回溯中提取行“有问题的”行号,也许通过一些源代码分析来确定执行成功跳转所需的断言前后的行号
- 再次运行测试,但这一次使用
Pdb
子类,该子类在断言之前的行上设置断点,并在命中断点时自动执行到第二个的跳转,然后c
继续。
或者,您可以自动为assert
测试中找到的每个断点设置断点(而不是等待声明失败)(再次使用源代码分析,您可以为ast.Assert
测试的AST 琐碎提取节点的行号),执行断言的测试使用调试器脚本命令,并使用该jump
命令跳过断言本身。您必须进行权衡;在调试器下运行所有测试(这很慢,因为解释器必须为每个语句调用跟踪函数),或者仅将其应用于失败的测试,并付出从头开始重新运行这些测试的代价。
这样的插件将需要大量工作来创建,我这里不会写一个例子,部分是因为它无论如何都不适合答案,部分是因为我认为这不值得。我只是打开调试器并手动进行跳转。断言失败表示测试本身或被测代码存在错误,因此您最好只专注于调试问题。