Get all logging output with mock

logging, mocking, python, unit-testing

Solution

stdlib

Since Python 3.4 the batteries' `unittest` has `assertLogs`. When used without `logger` and `level` arguments, it catches all logging (suppresses existing handlers). You can later access recorded entries from the context manager's `records` attribute. Text output strings are stored in `output` list.

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)

Tornado

For Python 2 I usually take Tornado's `ExpectLog`. It's self-contained and works for normal Python code. It's actually more elegant solution then stdlib's, because instead of several class, `ExpectLog` is just a normal `logging.Filter` (a class, source). But it lacks a couple of features, including access to recorded entries, so usually I also extend it a bit, like:

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")

Then you can have something like:

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)

However, note that for the filter approach current logging level is important (can be overridden with `level` argument), and also you need a filter per logger of interest. You can follow the approach and make something that fits your case better.

Update

Alternatively there's unittest2 backport for Python 2 which has `assertLogs`.

Problem

I want to get all logging output with mock. I searched, but only found ways to mock explicitly logging.info or logging.warn. I need all output, whatever logging level was set. ``` def test_foo(): def my_log(...): logs.append(...) with mock.patch('logging.???', my_log): ... ``` In our libraries we use this: ``` import logging logger=logging.getLogger(__name__) def foo(): logger.info(...) ```

Original source