【问题标题】:Testing warnings with doctest使用 doctest 测试警告
【发布时间】:2010-03-10 16:24:50
【问题描述】:

我想使用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 来测试警告吗?我在文档或我的 Google 搜索中找不到任何提及这一点的方法。

【问题讨论】:

    标签: python warnings doctest


    【解决方案1】:

    这不是最优雅的方式,但它对我有用:

    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,因为 UserWarning 输出中报告的路径必须以斜杠开头,就像我编写此测试的方式一样。你也许能想出一些更好的 ELLIPSIS 指令的咒语,但我不能。

    【讨论】:

      【解决方案2】:

      Python 文档的Testing Warnings 部分专门针对此主题。但是,总而言之,您有两种选择:

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

      【讨论】:

        【解决方案3】:

        您面临的问题是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

        如果您想要快速而肮脏的 hack,请组合一个将 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
        

        然后你可以在你的 doctest 中调用一个修饰函数:

        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.
        

        【讨论】:

          【解决方案4】:

          文档建议您可以在运行 doctest 时 pass -Wd 始终触发警告。

          【讨论】:

          • 谢谢,但我的文档字符串的目的是展示我的库的正常使用情况。而且由于大多数人不会将警告转化为错误,因此我不想在我的文档中将警告显示为异常。我希望有一种方法可以让我以通常的方式编写我的文档字符串,并且仍然可以测试警告。
          【解决方案5】:

          也许你可以试试mocking(补丁打印!)麻烦一点。我承认这会给文档字符串增加一些混乱,但它可能值得一试。

          如果您希望在保留当前语法的同时使用该方法,也许您可​​以尝试为 doctest 实现一个自定义包装器,该包装器生成缺少的代码,然后执行固定的测试。如果可能,最好避免这种情况。

          或者,您可以创建一个完全自定义的 doctest 运行器,但我想您更愿意避免这种情况。 :)

          【讨论】:

            【解决方案6】:

            将过滤器设置为“始终”是否可以满足需要?

            > cat warnme.py 
            import warnings
            
            for i in range(3):
                warnings.warn("oh noes!")
            
            > python warnme.py 
            warnme.py:4: UserWarning: oh noes!
            warnings.warn("oh noes!")
            
            > cat warnme2.py 
            import warnings
            
            warnings.simplefilter("always")
            for i in range(3):
                warnings.warn("oh noes!")
            
            > python warnme2.py 
            warnme2.py:5: UserWarning: oh noes!
            warnings.warn("oh noes!")
            warnme2.py:5: UserWarning: oh noes!
            warnings.warn("oh noes!")
            warnme2.py:5: UserWarning: oh noes!
            warnings.warn("oh noes!")
            

            【讨论】:

              【解决方案7】:

              这是为什么文档测试不适用于所有测试的一个示例。如果您的文档字符串中有内联示例并且需要对其进行测试,那是一回事,但正如您发现的那样,您要测试的某些行为最好通过字符串匹配来完成。以及不需要将文档字符串与所有测试机制混在一起的情况。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-02-23
                • 2012-03-14
                • 2017-03-06
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多