使用doctest测试警告
我想用 doctests 来测试某些警告是否出现。比如,我有一个这样的模块:
from warnings import warn
class Foo(object):
"""
Instantiating Foo always gives a warning:
>>> foo = Foo()
testdocs.py:14: UserWarning: Boo!
warn("Boo!", UserWarning)
>>>
"""
def __init__(self):
warn("Boo!", UserWarning)
如果我运行 python -m doctest testdocs.py
来执行我类中的 doctest,确保警告被打印出来,我得到的结果是:
testdocs.py:14: UserWarning: Boo!
warn("Boo!", UserWarning)
**********************************************************************
File "testdocs.py", line 7, in testdocs.Foo
Failed example:
foo = Foo()
Expected:
testdocs.py:14: UserWarning: Boo!
warn("Boo!", UserWarning)
Got nothing
**********************************************************************
1 items had failures:
1 of 1 in testdocs.Foo
***Test Failed*** 1 failures.
看起来警告是被打印出来了,但 doctest 并没有捕捉到或者注意到这个警告。我猜这可能是因为警告是打印到 sys.stderr
而不是 sys.stdout
。即使我在模块的最后写了 sys.stderr = sys.stdout
,这个情况还是会发生。
那么,有没有办法用 doctests 来测试警告呢?我在文档里和谷歌搜索中都没有找到相关的信息。
7 个回答
你遇到的问题是,warnings.warn()
会调用 warnings.showwarning()
,而这个函数会把 warnings.formatwarning()
的结果写入一个文件,默认是写到 sys.stderr
,也就是错误输出。
(参考链接:http://docs.python.org/library/warnings.html#warnings.showwarning)
如果你在用 Python 2.6,可以使用 warnings.catch_warnings()
这个上下文管理器,轻松修改警告的处理方式,包括临时替换 warnings.showwarning()
的实现,让它写到 sys.stdout
,也就是标准输出。这样做是比较正确的处理方式。
(参考链接:http://docs.python.org/library/warnings.html#available-context-managers)
如果你想要一个快速且简单的解决办法,可以写一个装饰器,把 sys.stderr
重定向到 sys.stdout
:
def stderr_to_stdout(func):
def wrapper(*args):
stderr_bak = sys.stderr
sys.stderr = sys.stdout
try:
return func(*args)
finally:
sys.stderr = stderr_bak
return wrapper
然后你可以在你的文档测试中调用这个装饰过的函数:
from warnings import warn
from utils import stderr_to_stdout
class Foo(object):
"""
Instantiating Foo always gives a warning:
>>> @stderr_to_stdout
... def make_me_a_foo():
... Foo()
...
>>> make_me_a_foo()
testdocs.py:18: UserWarning:
warn("Boo!", UserWarning)
>>>
"""
def __init__(self):
warn("Boo!", UserWarning)
这样就能通过测试了:
$ python -m doctest testdocs.py -v
Trying:
@stderr_to_stdout
def make_me_a_foo():
Foo()
Expecting nothing
ok
Trying:
make_me_a_foo()
Expecting:
testdocs.py:18: UserWarning: Boo!
warn("Boo!", UserWarning)
ok
[...]
2 passed and 0 failed.
Python文档中有一个专门讲解这个主题的部分,叫做测试警告。不过,简单来说,你有两个选择:
(A) 使用 catch_warnings
上下文管理器
这是官方文档推荐的方法。不过,catch_warnings
这个上下文管理器是在Python 2.6版本才出现的。
import warnings
def fxn():
warnings.warn("deprecated", DeprecationWarning)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
# Trigger a warning.
fxn()
# Verify some things
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated" in str(w[-1].message)
(B) 将警告升级为错误
如果这个警告之前没有出现过——也就是说它已经被记录在警告注册表中——那么你可以设置警告为引发异常,并捕获它。
import warnings
def fxn():
warnings.warn("deprecated", DeprecationWarning)
if __name__ == '__main__':
warnings.simplefilter("error", DeprecationWarning)
try:
fxn()
except DeprecationWarning:
print "Pass"
else:
print "Fail"
finally:
warnings.simplefilter("default", DeprecationWarning)
这不是最优雅的方法,但对我来说有效:
from warnings import warn
class Foo(object):
"""
Instantiating Foo always gives a warning:
>>> import sys; sys.stderr = sys.stdout
>>> foo = Foo() # doctest:+ELLIPSIS
/.../testdocs.py:14: UserWarning: Boo!
warn("Boo!", UserWarning)
"""
def __init__(self):
warn("Boo!", UserWarning)
if __name__ == '__main__':
import doctest
doctest.testmod()
不过,这个方法在Windows上可能不太好用,因为我写的测试中,用户警告输出的路径必须以斜杠开头。你可能能找到更好的使用ELLIPSIS指令的方法,但我没有找到。