Тестирование предупреждений с 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 для тестирования на предупреждения? Я не могу найти упоминание об этом пути или другом в документации или в моем поиске Google.

9
задан Eli Courtwright 10 March 2010 в 16:24
поделиться

6 ответов

Это не самый элегантный способ сделать это, но он работает для меня:

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, но я не смог.

4
ответ дан 4 December 2019 в 23:05
поделиться

docs предполагают, что вы можете передать -Wd при запуске doctest, чтобы всегда вызывать предупреждения.

0
ответ дан 4 December 2019 в 23:05
поделиться

Возможно, вы могли бы попробовать высмеять (напечатать!) проблемный бит. Я признаю, что это внесет некоторый беспорядок в документацию, но, возможно, стоит попробовать.

Если вы хотите использовать этот подход, сохраняя текущий синтаксис, возможно, вы могли бы попробовать реализовать пользовательскую обертку для doctest, которая генерирует недостающий код, а затем выполняет исправленные тесты. Если возможно, этого лучше избегать.

В качестве альтернативы вы можете просто создать полностью собственный doctest runner, но я полагаю, что вы предпочтете избежать этого. :)

.
0
ответ дан 4 December 2019 в 23:05
поделиться

Проблема, с которой вы столкнулись, заключается в том, что 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

Затем вы можете вызвать декорированную функцию в вашем 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.
1
ответ дан 4 December 2019 в 23:05
поделиться

Это один из примеров того, почему доктесты подходят не для всех тестов. Если у вас есть встроенные примеры в ваших строках документации, и их нужно протестировать, это одно, но, как вы обнаружили, есть варианты поведения, которые вы хотите проверить, и которые не лучше всего делать с сопоставлением строк. И случаи, когда вам не нужно загромождать строку документации всеми механизмами тестирования.

-1
ответ дан 4 December 2019 в 23:05
поделиться

Разделы Предупреждения о тестировании документации по 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) Обновление предупреждений до errors

Если предупреждение не было замечено ранее и, следовательно, было зарегистрировано в реестре предупреждений, то вы можете установить предупреждения для создания исключений и их перехвата.

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
ответ дан 4 December 2019 в 23:05
поделиться
Другие вопросы по тегам:

Похожие вопросы: