使用模拟获取所有日志记录输出

gue*_*tli 7 python logging unit-testing mocking

我想使用模拟获取所有日志记录输出。我进行了搜索,但只找到了显式模拟logging.info或logging.warn的方法。

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

def test_foo():

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

   with mock.patch('logging.???', my_log):
        ...
Run Code Online (Sandbox Code Playgroud)

在我们的库中,我们使用以下代码:

import logging
logger=logging.getLogger(__name__)

def foo():
    logger.info(...)
Run Code Online (Sandbox Code Playgroud)

hoe*_*ing 9

pytest

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

# 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
Run Code Online (Sandbox Code Playgroud)

安装

灯具最初是在pytest名为的插件中引入的,pytest-capturelog现在已被放弃。幸运的是,它有一个名为的体面的叉子pytest-catchlog,最近已被合并到其中pytest==3.3.0。因此,如果您使用的最新版本pytest,则已经可以使用了;对于较旧的版本pytestpytest-catchlog从PyPI 安装

文件

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

平原 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()
Run Code Online (Sandbox Code Playgroud)

然后,您可以扩展自定义处理程序和执行任何你需要的逻辑,就像在收集记录数据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)
Run Code Online (Sandbox Code Playgroud)


saa*_*aaj 8

标准库

因为Python 3.4的电池unittestassertLogs。当不带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)
Run Code Online (Sandbox Code Playgroud)

龙卷风

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

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")
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

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

更新

或者,还有适用于 Python 2的unittest2 backport,它具有assertLogs.