PyDev unittesting:如何在“捕获的输出”中捕获记录到logging.Logger的文本

时间:2011-09-19 14:54:18

标签: python unit-testing logging pydev

我正在使用PyDev进行Python应用程序的开发和单元测试。 至于单元测试,一切都很好,除了没有内容记录到日志框架。记录器不会被PyDev的“捕获输出”捕获。

我已经将所有记录到标准输出的内容转发如下:

import sys
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))

然而,“捕获的输出”不会显示记录到记录器的内容。

这是一个示例unittest-script: test.py

import sys
import unittest
import logging

logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))

class TestCase(unittest.TestCase):
    def testSimpleMsg(self):
        print("AA")
        logging.getLogger().info("BB")

控制台输出是:

Finding files... done.
Importing test modules ... done.

testSimpleMsg (itf.lowlevel.tests.hl7.TestCase) ... AA
2011-09-19 16:48:00,755 - root - INFO - BB
BB
ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

但测试的 CAPTURED OUTPUT 是:

======================== CAPTURED OUTPUT =========================
AA

有人知道如何捕获在执行此测试期间记录到logging.Logger的所有内容吗?

8 个答案:

答案 0 :(得分:54)

问题是unittest选手在测试开始前替换sys.stdout / sys.stderr,而StreamHandler仍在写入原始sys.stdout

如果您将“当前”sys.stdout分配给处理程序,它应该有效(请参阅下面的代码)。

import sys
import unittest
import logging

logger = logging.getLogger()
logger.level = logging.DEBUG
stream_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stream_handler)

class TestCase(unittest.TestCase):
    def testSimpleMsg(self):
        stream_handler.stream = sys.stdout
        print("AA")
        logging.getLogger().info("BB")

尽管如此,更好的方法是在测试期间添加/删除处理程序:

import sys
import unittest
import logging

logger = logging.getLogger()
logger.level = logging.DEBUG

class TestCase(unittest.TestCase):
    def testSimpleMsg(self):
        stream_handler = logging.StreamHandler(sys.stdout)
        logger.addHandler(stream_handler)
        try:
            print("AA")
            logging.getLogger().info("BB")
        finally:
            logger.removeHandler(stream_handler)

答案 1 :(得分:17)

我已经厌倦了必须手动将Fabio's great code添加到所有setUp,因此我将unittest.TestCase与[{1}}个__metaclass__分组:

class LoggedTestCase(unittest.TestCase):
    __metaclass__ = LogThisTestCase
    logger = logging.getLogger("unittestLogger")
    logger.setLevel(logging.DEBUG) # or whatever you prefer

class LogThisTestCase(type):
    def __new__(cls, name, bases, dct):
        # if the TestCase already provides setUp, wrap it
        if 'setUp' in dct:
            setUp = dct['setUp']
        else:
            setUp = lambda self: None
            print "creating setUp..."

        def wrappedSetUp(self):
            # for hdlr in self.logger.handlers:
            #    self.logger.removeHandler(hdlr)
            self.hdlr = logging.StreamHandler(sys.stdout)
            self.logger.addHandler(self.hdlr)
            setUp(self)
        dct['setUp'] = wrappedSetUp

        # same for tearDown
        if 'tearDown' in dct:
            tearDown = dct['tearDown']
        else:
            tearDown = lambda self: None

        def wrappedTearDown(self):
            tearDown(self)
            self.logger.removeHandler(self.hdlr)
        dct['tearDown'] = wrappedTearDown

        # return the class instance with the replaced setUp/tearDown
        return type.__new__(cls, name, bases, dct)

现在,您的测试用例可以简单地从LoggedTestCase继承,即class TestCase(LoggedTestCase)而不是class TestCase(unittest.TestCase),您就完成了。或者,您可以添加__metaclass__行并在测试中定义logger或稍加修改LogThisTestCase

答案 2 :(得分:7)

我建议使用LogCapture并测试您是否记录了您希望记录的内容:

http://testfixtures.readthedocs.org/en/latest/logging.html

答案 3 :(得分:2)

有些人可能访问此线程以找到一种将测试期间创建的日志转发到控制台或 PyDev 的方法。以上答案已经提供了一些解决方案。

如果想要在实际测试中捕获特定日志,我发现自 Python 3.4 起,unittest.TestCase 提供了 assertLogs(),它返回一个上下文管理器来捕获当前日志消息。来自unittest docs

with self.assertLogs('foo', level='INFO') as cm:
   logging.getLogger('foo').info('first message')
   logging.getLogger('foo.bar').error('second message')
self.assertEqual(cm.output, ['INFO:foo:first message',
                             'ERROR:foo.bar:second message'])

消息在 cm.output 中捕获。有关更详细的信息(如时间、文件、行号等),cm.records 包含 LogRecords 的列表。

所有这些都没有直接解决 PyDev 面临的 OP,而是提供了一种以编程方式检查创建的消息的方法。

对于那些熟悉 pytest 的人,可以使用 --log-cli-level=LEVEL 标志(例如 pytest --log-cli-level=info)将格式良好的日志消息转发到控制台。

答案 4 :(得分:1)

我也遇到过这个问题。我最终继承了StreamHandler,并使用获取sys.stdout的属性覆盖了stream属性。这样,处理程序将使用unittest.TestCase已交换到sys.stdout的流:

class CapturableHandler(logging.StreamHandler):

    @property
    def stream(self):
        return sys.stdout

    @stream.setter
    def stream(self, value):
        pass

然后您可以在运行测试之前设置日志记录处理程序(这会将自定义处理程序添加到根记录器):

def setup_capturable_logging():
    if not logging.getLogger().handlers:
        logging.getLogger().addHandler(CapturableHandler())

如果像我一样,你在单独的模块中进行测试,你可以在每个单元测试模块的导入之后放一行,以确保在测试运行之前设置日志记录:

import logutil

logutil.setup_capturable_logging()

这可能不是最干净的方法,但它非常简单,对我来说效果很好。

答案 5 :(得分:1)

在阅读了本主题以及其他一些相关主题(谢谢!)中的答案之后,这是我整理的上下文管理器,它将捕获记录器的输出(如果已发送)。

from io import StringIO
import logging
class CaptureLogger:
    """Context manager to capture `logging` streams

    Args:
        - logger: 'logging` logger object

    Results:
        The captured output is available via `self.out`

    """

    def __init__(self, logger):
        self.logger = logger
        self.io = StringIO()
        self.sh = logging.StreamHandler(self.io)
        self.out = ''

    def __enter__(self):
        self.logger.addHandler(self.sh)
        return self

    def __exit__(self, *exc):
        self.logger.removeHandler(self.sh)
        self.out = self.io.getvalue()

    def __repr__(self):
        return f"captured: {self.out}\n"

用法示例:

logger = logging.getLogger()
msg = "Testing 1, 2, 3"
with CaptureLogger(logger) as cl:
    logger.error(msg)
assert cl.out, msg+"\n"

由于OP要求将其放入捕获的stdout流中,因此您可以将其打印到__exit__中的stdout中,因此可以添加如下一行:

    def __exit__(self, *exc):
        self.logger.removeHandler(self.sh)
        self.out = self.io.getvalue()
        print(self.out)

此解决方案的不同之处在于,它将在所有常规print()调用(如果有)之后立即收集日志输出并一次将其全部转储出去。因此,可能不是OP所追求的,但这可以很好地满足我的需求。

答案 6 :(得分:0)

如果您有用于测试,开发和生产的其他初始化模块,则可以禁用任何内容或将其重定向到初始化程序中。 我有local.py,test.py和production.py,它们都继承自common.y

common.py进行包括以下代码段在内的所有主要配置:

    LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'django.server': {
            '()': 'django.utils.log.ServerFormatter',
            'format': '[%(server_time)s] %(message)s',
        },
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'django.server': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'django.server',
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': True,
        },
        'celery.tasks': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        },
        'django.server': {
            'handlers': ['django.server'],
            'level': 'INFO',
            'propagate': False,
        },
    }

然后在test.py中,我有这个:

console_logger = Common.LOGGING.get('handlers').get('console')
console_logger['class'] = 'logging.FileHandler
console_logger['filename'] = './unitest.log

这用FileHandler替换了控制台处理程序,意味着仍然可以记录日志,但是我不必接触生产代码库。

答案 7 :(得分:-1)

这是一个小技巧,但对我有用。当您要显示捕获的日志时,请添加此代码。不需要后将其删除。

self.assertEqual(1, 0)

示例:

def test_test_awesome_function():
    print("Test 1")
    logging.info("Test 2")
    logging.warning("Test 3")

    self.assertEqual(1, 0)