如何在unittest中验证python日志格式?

时间:2019-01-10 09:28:17

标签: python unit-testing logging pytest

最近,我正在编写一个python日志记录扩展程序,我想为该扩展程序添加一些测试,以验证我的扩展程序是否按预期工作。
但是,我不知道如何捕获完整的日志并与unittest / pytest中的例外结果进行比较。

简化样本

# app.py
import logging
def create_logger():
    formatter = logging.Formatter(fmt='%(name)s-%(levelname)s-%(message)s')
    hdlr = logging.StreamHandler()
    hdlr.setFormatter(formatter)
    logger = logging.getLogger(__name__)
    logger.setLevel('DEBUG')
    logger.addHandler(hdlr)
    return logger


app_logger = create_logger()

这是我的测试

尝试1:单元测试

from app import app_logger
import unittest


class TestApp(unittest.TestCase):

    def test_logger(self):
        with self.assertLogs('', 'DEBUG') as cm:
            app_logger.debug('hello')
        # or some other way to capture the log output.
        self.assertEqual('app-DEBUG-hello', cm.output)
  • 预期行为:
    cm.output = 'app-DEBUG-hello'
    
  • 实际行为
    cm.output = ['DEBUG:app:hello']
    

尝试2:pytest Caplog

from app import app_logger
import pytest


def test_logger(caplog):
    app_logger.debug('hello')
    assert caplog.text == 'app-DEBUG-hello'
  • 预期行为:
    caplog.text = 'app-DEBUG-hello'
    
  • 实际行为
    caplog.text = 'test_logger.py               6 DEBUG    hello'
    

尝试3:pytest capsys

从应用程序导入app_logger 导入pytest

def test_logger(capsys):
    app_logger.debug('hello')
    out, err = capsys.readouterr()
    assert err
    assert err == 'app-DEBUG-hello'
  • 预期行为:
    err = 'app-DEBUG-hello'
    
  • 实际行为
    err = ''
    

考虑到会有许多格式不同的测试,我不想手动检查日志格式。我不知道如何获取控制台上看到的完整日志,并将其与测试用例中的预期日志进行比较。希望您的帮助,谢谢。

4 个答案:

答案 0 :(得分:1)

我知道这很旧,但是在这里发布,因为它在Google中为我拉起了...

可能需要清理,但这对我来说已经很近了,所以我认为分享是一件好事。

这是我组合在一起的一个测试用例,可以让我通过复制格式化程序来验证特定的处理程序是否按预期进行了格式化:

import io
import logging

from django.conf import settings
from django.test import SimpleTestCase
from django.utils.log import DEFAULT_LOGGING

class SetupLoggingMixin:
    def setUp(self):
        super().setUp()
        logging.config.dictConfig(settings.LOGGING)
        self.stream = io.StringIO()
        self.root_logger = logging.getLogger("")
        self.root_hdlr = logging.StreamHandler(self.stream)

        console_handler = None
        for handler in self.root_logger.handlers:
            if handler.name == 'console':
                console_handler = handler
                break

        if console_handler is None:
            raise RuntimeError('could not find console handler')

        formatter = console_handler.formatter
        self.root_formatter = formatter
        self.root_hdlr.setFormatter(self.root_formatter)
        self.root_logger.addHandler(self.root_hdlr)

    def tearDown(self):
        super().tearDown()
        self.stream.close()
        logging.config.dictConfig(DEFAULT_LOGGING)

这是使用方法的示例:

class SimpleLogTests(SetupLoggingMixin, SimpleTestCase):
    def test_logged_time(self):
        msg = 'foo'
        self.root_logger.error(msg)
        self.assertEqual(self.stream.getvalue(), 'my-expected-message-formatted-as-expected')

答案 1 :(得分:0)

在阅读unittest库的源代码之后,我制定了以下绕过方法。请注意,它可以通过更改导入模块的受保护成员来工作,因此将来的版本中可能会破坏它。

from unittest.case import _AssertLogsContext
_AssertLogsContext.LOGGING_FORMAT = 'same format as your logger'

执行这些命令后,self.assertLogs打开的日志上下文将使用上述格式。我真的不知道为什么将这些值保留为硬编码且不可配置。

我没有找到读取记录器格式的选项,但是如果您使用logging.config.dictConfig,则可以使用同一词典中的值。

答案 2 :(得分:0)

我知道这并不能完全回答OP的问题,但是我在寻找一种捕获日志消息的巧妙方法时偶然发现了这篇文章。

采用@ user319862的功能,我已经对其进行了清理和简化。

import unittest
import logging

from io import StringIO

class SetupLogging(unittest.TestCase):
    def setUp(self):
        super().setUp()
        self.stream = StringIO()
        self.root_logger = logging.getLogger("")
        self.root_hdlr = logging.StreamHandler(self.stream)
        self.root_logger.addHandler(self.root_hdlr)

    def tearDown(self):
        super().tearDown()
        self.stream.close()

    def test_log_output(self):
        """ Does the logger produce the correct output? """
        msg = 'foo'
        self.root_logger.error(msg)
        self.assertEqual(self.stream.getvalue(), 'foo\n')


if __name__ == '__main__':
    unittest.main()

答案 3 :(得分:0)

我是 Python 新手,但在其他语言的测试/tdd 方面有一些经验,发现“更改”格式化程序的默认方式是添加新的流处理程序,但如果您已经在记录器中定义了流(即使用 Azure 函数或 TestCase::assertLogs 为您添加一个)您最终会记录两次,一次使用您的格式,另一次使用“默认”格式。

如果在 OP 中,函数 create_logger 改变了当前 StreamHandler 的格式化程序,而不是添加一个新的 StreamHandler(检查是否存在,如果不创建一个新的和所有那个爵士乐...)

然后您可以在 create_logger 之后调用 with self.assertLogs('', 'DEBUG') as cm: 并断言 cm.output 并且它只是有效,因为您正在改变 FormatterStreamHandler assertLogs 正在添加。

所以基本上发生的事情是执行顺序不适合测试。

OP中的执行顺序是:

  • 导入东西
    • 将流添加到记录器格式化程序
  • 运行测试
    • 通过 self.assertLogs 向记录器格式化程序添加另一个流
    • 在第二个 StreamHandler 中声明内容

应该什么时候 执行顺序为:

  • 导入东西
  • 使用记录器格式化程序添加流(但无关紧要)
  • 运行测试
  • 通过 self.assertLogs 添加另一个带有记录器格式化程序的流
  • 更改当前流记录器格式化程序
  • 仅在格式正确的 StreamHandler 中断言内容