如何使用doctest进行日志记录?

时间:2014-03-10 13:02:07

标签: python logging doctest

以下doctest失败:

import logging
logging.basicConfig(level=logging.DEBUG,format='%(message)s')

def say_hello():
  '''
  >>> say_hello()
  Hello!
  '''
  logging.info('Hello!')

if __name__ == '__main__':
    import doctest
    doctest.testmod()

这些页面

似乎建议logging.StreamHandler(sys.stdout)logger.addHandler(handler),但我的尝试在这方面失败了。 (如果不是很明显的话,我是python的新手。)

请帮我修复上述代码,以便测试通过。


2017年6月4日更新:回答00prometheus'评论:当我提出这个问题时,use doctest and logging in python program接受的答案似乎不必要地复杂化了。确实如此,因为这里公认的答案提供了一个更简单的解决方案。在我高度偏见的观点中,我的问题也比我在原帖中已经链接的问题更清晰。

3 个答案:

答案 0 :(得分:2)

您需要定义“记录器”对象。这通常在导入后执行:

import sys
import logging
log = logging.getLogger(__name__)

如果要记录消息:

log.info('Hello!')

在像脚本一样运行的代码中设置basicConfig:

if __name__ == '__main__':
    import doctest
    logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format='%(message)s')
    docttest.testmod()

修改

好的,你是对的。它不起作用,但我让它工作......但是不要这样做!只需使用print语句或返回您实际需要检查的内容。正如你的第二个链接所说,这只是一个坏主意。您不应该检查日志记录输出(用于记录日志)。即使是第二个链接的原始海报也表示他们通过将日志切换为使用打印来实现它。但这里似乎是邪恶的代码:

class MyDocTestRunner(doctest.DocTestRunner):
    def run(self, test, compileflags=None, out=None, clear_globs=True):
        if out is None:
            handler = None
        else:
            handler = logging.StreamHandler(self._fakeout)
            out = sys.stdout.write
        logger = logging.getLogger() # root logger (say)
        if handler:
            logger.addHandler(handler)
        try:
            doctest.DocTestRunner.run(self, test, compileflags, out, clear_globs)
        finally:
            if handler:
                logger.removeHandler(handler)
                handler.close()

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG, format='%(message)s')
    tests = doctest.DocTestFinder().find(say_hello, __name__)
    dt_runner = MyDocTestRunner()
    for t in tests:
        dt_runner.run(t, out=True)

修改(续):

尝试第二个链接时,我的尝试也失败了。这是因为内部doctest将sys.stdout重新分配给self._fakeout。这就是为什么我的黑客不会起作用的原因。我实际上告诉记录器写这个“假冒”。

修改(回复评论):

这不完全是链接中的代码。如果是链接中的代码,我会说它不是一个选项,因为它没有做任何太复杂的事情。但是,我的代码使用的是“私有”内部实例属性,普通用户不应使用该属性。这就是为什么它是邪恶的。

是的,日志记录可以用于测试输出,但是在单元测试/ doctest中这样做没有多大意义,这可能是doctest不包含开箱即用功能的原因。您链接到的TextTest内容都与功能或集成测试有关。 Unittests(和doctests)应该测试小的单个组件。如果你必须捕获记录的输出以确保你的unittest / doctest是正确的,那么你应该考虑将事情分开或不在doctest中进行这些检查。

我个人仅使用doctests进行简单的示例和验证。主要用于使用示例,因为任何用户都可以看到内联doctest。

编辑(ok last one):

相同的解决方案,更简单的代码。此代码不要求您创建自定义运行器。您仍然需要创建默认的跑步者和东西,因为您需要访问“_fakeout”属性。如果不将此属性作为流记录,则无法使用doctest检查日志记录输出。

if __name__ == '__main__':
    dt_runner = doctest.DocTestRunner()
    tests = doctest.DocTestFinder().find(sys.modules[__name__])
    logging.basicConfig(level=logging.DEBUG, format='%(message)s', stream=dt_runner._fakeout)
    for t in tests:
        dt_runner.run(t)

答案 1 :(得分:0)

一种方法是通过猴子修补logging模块(我的代码; import logging中的文档字符串内容与您的问题有关)

@classmethod
def yield_int(cls, field, text):
    """Parse integer values and yield (field, value)

    >>> test = lambda text: dict(Monster.yield_int('passive', text))
    >>> test(None)
    {}
    >>> test('42')
    {'passive': 42}
    >>> import logging
    >>> old_warning = logging.warning
    >>> warnings = []
    >>> logging.warning = lambda msg: warnings.append(msg)
    >>> test('seven')
    {}
    >>> warnings
    ['yield_int: failed to parse text "seven"']
    >>> logging.warning = old_warning
    """
    if text == None:
        return

    try:
        yield (field, int(text))
    except ValueError:
        logging.warning(f'yield_int: failed to parse text "{text}"')

但是,更清洁的方法是使用unittest模块:

    >>> from unittest import TestCase
    >>> with TestCase.assertLogs(_) as cm:
    ...     print(test('seven'))
    ...     print(cm.output)
    {}
    ['WARNING:root:yield_int: failed to parse text "seven"']

从技术上讲,您可能应该实例化一个TestCase对象,而不是将_传递给assertLogsself,因为不能保证此方法不会尝试访问实例属性。

答案 2 :(得分:0)

我使用以下技术:

  1. 将日志流设置为 StringIO 对象。
  2. 退出...
  3. 打印 StringIO 对象的内容并期待输出。
  4. 或:对 StringIO 对象的内容进行断言。

应该这样做。

这是一些示例代码。

首先,它只是为 doctest 中的日志记录进行整个设置 - 只是为了展示它是如何工作的。

然后代码显示了如何将设置放入单独的函数 setup_doctest_logging 中,该函数执行设置 à 并返回一个打印日志的函数。这使测试代码更加集中,并将仪式部分移出测试。

import logging


def func(s):
    """
    >>> import io
    >>> string_io = io.StringIO()
    >>> # Capture the log output to a StringIO object
    >>> # Use force=True to make this configuration stick
    >>> logging.basicConfig(stream=string_io, format='%(message)s', level=logging.INFO, force=True)

    >>> func('hello world')

    >>> # print the contents of the StringIO. I prefer that. Better visibility.
    >>> print(string_io.getvalue(), end='')
    hello world
    >>> # The above needs the end='' because print will otherwise add an new line to the
    >>> # one that is already in the string from logging itself

    >>> # Or you can just expect an extra empty line like this:
    >>> print(string_io.getvalue())
    hello world
    <BLANKLINE>

    >>> func('and again')

    >>> # Or just assert on the contents.
    >>> assert 'and again' in string_io.getvalue()
    """
    logging.info(s)


def setup_doctest_logging(format='%(levelname)s %(message)s', level=logging.WARNING):
    """ 
    This could be put into a separate module to make the logging setup easier
    """
    import io
    string_io = io.StringIO()
    logging.basicConfig(stream=string_io, format=format, level=level, force=True)

    def log_printer():
        s = string_io.getvalue()
        print(s, end='')
    return log_printer


def other_logging_func(s, e=None):
    """
    >>> print_whole_log = setup_doctest_logging(level=logging.INFO)
    >>> other_logging_func('no error')
    >>> print_whole_log()
    WARNING no error
    >>> other_logging_func('I try hard', 'but I make mistakes')
    >>> print_whole_log()
    WARNING no error
    WARNING I try hard
    ERROR but I make mistakes
    """
    logging.warning(s)
    if e is not None:
        logging.error(e)


if __name__ == '__main__':
    import doctest
    doctest.testmod()