【问题标题】:Get all logging output with mock使用 mock 获取所有日志记录输出
【发布时间】:2014-03-26 10:08:57
【问题描述】:

我想通过 mock 获取所有日志输出。我搜索了,但是 只能找到显式模拟 logging.info 或 logging.warn 的方法。

我需要所有输出,无论设置了什么日志记录级别。

def test_foo():

   def my_log(...):
      logs.append(...)

   with mock.patch('logging.???', my_log):
        ...

在我们的库中,我们使用这个:

import logging
logger=logging.getLogger(__name__)

def foo():
    logger.info(...)

【问题讨论】:

    标签: python unit-testing logging mocking


    【解决方案1】:

    pytest

    如果您使用pytest 编写测试,请查看一个名为caplog 的简洁夹具,它将为您捕获日志记录。它捕获所有发出的日志记录,然后您可以通过caplog.records 列表访问这些记录。每个元素都是logging.LogRecord 的一个实例,因此您可以轻松访问任何LogRecords attributes。示例:

    # spam.py
    
    import logging
    logger=logging.getLogger(__name__)
    
    def foo():
        logger.info('bar')
    
    
    # tests.py
    
    import logging
    from spam import foo
    
    def test_foo(caplog):
        foo()
        assert len(caplog.records) == 1
        record = next(iter(caplog.records))
        assert record.message == 'bar'
        assert record.levelno == logging.INFO
        assert record.module == 'spam'
        # etc
    

    安装

    夹具最初是在名为 pytest-capturelogpytest 插件中引入的,该插件现已被废弃。幸运的是,它最近有了一个不错的分支,名为 pytest-catchlogwhich has been merged into pytest==3.3.0。因此,如果您使用最新版本的pytest,您已经可以使用了;对于旧版本的pytestinstall pytest-catchlog from PyPI

    文档

    目前,pytest 没有为caplog 夹具提供任何文档(或者至少我找不到任何文档),因此您可以参考pytest-catchlogdocumentation

    普通unittest

    如果pytest 不是一个选项,我根本不会修补logging - 您可以简单地添加一个自定义处理程序来记录所有传入日志。一个小例子:

    # utils.py
    
    import logging
    
    
    class RecordsCollector(logging.Handler):
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.records = []
    
        def emit(self, record):
            self.records.append(record)
    
    
    # tests.py
    
    import logging
    import unittest
    from utils import RecordsCollector
    from spam import foo
    
    
    class SpamTests(unittest.TestCase):
    
        def setUp(self):
            self.collector = RecordsCollector()
            logging.getLogger('spam').addHandler(self.collector)
    
        def tearDown(self):
            logging.getLogger('spam').removeHandler(self.collector)
    
        def test_foo(self):
            foo()
            # same checks as in the example above
            self.assertEqual(len(self.collector.records), 1)
            record = next(iter(self.collector.records))
            self.assertEqual(record.message, 'bar')
            self.assertEqual(record.levelno, logging.INFO)
            self.assertEqual(record.module, 'spam')
    
    
    if __name__ == '__main__':
        unittest.main()
    

    然后您可以扩展自定义处理程序并实现您需要的任何逻辑,例如在将日志级别映射到记录列表的 dict 中收集记录,或者添加 contextmanager 实现,以便您可以开始和停止捕获测试内的记录:

    from contextlib import contextmanager
    
    @contextmanager
    def record_logs():
        collector = RecordsCollector()
        logging.getLogger('spam').addHandler(collector)
        yield collector
        logging.getLogger('spam').removeHandler(collector)
    
    
    def test_foo(self):
        with utils.record_logs() as collector:
            foo()
            self.assertEqual(len(collector.records), 1)
    

    【讨论】:

    • 好消息!到目前为止,我使用的是旧版本,但这是升级的原因。谢谢你的回答。
    • 很高兴能帮上忙!为了完整起见,我还为unittest-tests 添加了一个解决方案建议。尽管该提案主要模仿了caplog 夹具实现...
    • pytest-caplog-fixture 的文档可以在这里找到:docs.pytest.org/en/latest/logging.html 感谢unittest - 示例。这对我很有效。
    【解决方案2】:

    标准库

    从 Python 3.4 开始,电池的 unittest 具有 assertLogs。当不使用 loggerlevel 参数时,它会捕获所有日志记录(抑制现有处理程序)。您可以稍后从上下文管理器的records 属性访问记录的条目。文本输出字符串存储在output 列表中。

    import logging
    import unittest
    
    
    class TestLogging(unittest.TestCase):
    
        def test(self):
            with self.assertLogs() as ctx:
                logging.getLogger('foo').info('message from foo')
                logging.getLogger('bar').info('message from bar')
            print(ctx.records)
    

    龙卷风

    对于 Python 2,我通常使用 Tornado 的 ExpectLog。它是独立的,适用于普通的 Python 代码。它实际上是比标准库更优雅的解决方案,因为ExpectLog 不是几个类,而是一个普通的logging.Filter(一个类,source)。但是它缺少一些功能,包括访问记录的条目,所以通常我也会对其进行一些扩展,例如:

    class ExpectLog(logging.Filter):
    
        def __init__(self, logger, regex, required=True, level=None):
            if isinstance(logger, basestring):
                logger = logging.getLogger(logger)
            self.logger = logger
            self.orig_level = self.logger.level
            self.level = level
            self.regex = re.compile(regex)
            self.formatter = logging.Formatter()
            self.required = required
            self.matched = []
            self.logged_stack = False
    
        def filter(self, record):
            if record.exc_info:
                self.logged_stack = True
            message = self.formatter.format(record)
            if self.regex.search(message):
                self.matched.append(record)
                return False
            return True
    
        def __enter__(self):
            self.logger.addFilter(self)
            if self.level:
                self.logger.setLevel(self.level)
            return self
    
        def __exit__(self, typ, value, tb):
            self.logger.removeFilter(self)
            if self.level:
                self.logger.setLevel(self.orig_level)
            if not typ and self.required and not self.matched:
                raise Exception("did not get expected log message")
    

    然后你可以有类似的东西:

    class TestLogging(unittest.TestCase):
    
        def testTornadoself):
            logging.basicConfig(level = logging.INFO)
    
            with ExpectLog('foo', '.*', required = False) as ctxFoo:
                with ExpectLog('bar', '.*', required = False) as ctxBar:
                    logging.getLogger('foo').info('message from foo')
                    logging.getLogger('bar').info('message from bar')
            print(ctxFoo.matched)
            print(ctxBar.matched)
    

    但是,请注意,对于过滤器方法,当前日志记录级别很重要(可以用 level 参数覆盖),并且您还需要为每个感兴趣的记录器设置一个过滤器。您可以按照该方法制作更适合您情况的内容。

    更新

    另外还有一个用于 Python 2 的 unittest2 反向端口,它具有 assertLogs

    【讨论】:

    • 我最喜欢 stdlib 解决方案。到目前为止,我仍然固定在 Python 2.7 上,但迟早我们会在那里。谢谢你的回答。
    • 通过引用 unittest2 backport 更新了答案,因此您始终使用相同的方法。
    • 很好,一个反向移植。是的,我们仍然使用 Python2.7。我问自己为什么 Python 世界有这么多选择。有时这很好,有时令人困惑:-) 在这种情况下:unittest2 vs pytest。
    • 是否可以假设log.records 的顺序与记录日志的顺序相同?比如不是按级别排序的?
    • 我认为可以这样假设。目标记录器的Here handlers 临时更改为_CapturingHandler 的实例,其中其emit 只是附加到两个列表,原始记录记录到一个,它们的格式化表示到另一个。
    【解决方案3】:

    模块 testfixtures 有一个类来处理这个:

    >>> import logging
    >>> from testfixtures import LogCapture
    >>> with LogCapture() as l:
    ...     logger = logging.getLogger()
    ...     logger.info('a message')
    ...     logger.error('an error')
    
    >>> l.check(
    ...     ('root', 'INFO', 'a message'),
    ...     ('root', 'ERROR', 'another error'),
    ...     )
    Traceback (most recent call last):
     ...
    AssertionError: sequence not as expected:
    
    same:
    (('root', 'INFO', 'a message'),)
    
    expected:
    (('root', 'ERROR', 'another error'),)
    
    actual:
    (('root', 'ERROR', 'an error'),)
    

    来源:http://testfixtures.readthedocs.io/en/latest/logging.html

    【讨论】:

      【解决方案4】:

      我找到了这个解决方案:

      def test_foo(self):
      
          logs=[]
      
          def my_log(self, *args, **kwargs):
              logs.append((args, kwargs))
      
          with mock.patch('logging.Logger._log', my_log):
              ...
      

      【讨论】:

      • 我觉得这个方案太复杂了。
      猜你喜欢
      • 2011-11-11
      • 2021-05-11
      • 1970-01-01
      • 2013-09-18
      • 1970-01-01
      • 2019-10-26
      • 2020-11-03
      • 2011-11-02
      • 2014-12-25
      相关资源
      最近更新 更多