使用doctest测试警告

15 投票
7 回答
2163 浏览
提问于 2025-04-15 20:15

我想用 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 个回答

2

你遇到的问题是,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.
5

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)
4

这不是最优雅的方法,但对我来说有效:

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指令的方法,但我没有找到。

撰写回答