Python 2.7 Unit test: Assert logger warning thrown

前端 未结 2 1382
逝去的感伤
逝去的感伤 2021-01-08 00:41

I\'m trying to write a Unit Test for a piece of python code that raises a warning via logger.warn(\'...\') under certain conditions. How do I assert that this w

相关标签:
2条回答
  • 2021-01-08 00:58

    Python 3.4 Added to unittest exactly that feature. See TestCase.assertLogs. The API is really easy to use:

    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'])
    

    Now, this question is tagged python2.7 but it will show up when search for similar title for python + unittest + logging. And it's pretty easy to back-port that feature to Python2.7, so here it is:

    # logger_test.py 
    # this file contains the base class containing the newly added method
    # assertLogs
    import collections
    import logging
    _LoggingWatcher = collections.namedtuple("_LoggingWatcher",
                                             ["records", "output"])
    
    class _BaseTestCaseContext(object):
    
        def __init__(self, test_case):
            self.test_case = test_case
    
        def _raiseFailure(self, standardMsg):
            msg = self.test_case._formatMessage(self.msg, standardMsg)
            raise self.test_case.failureException(msg)
    
    
    class _CapturingHandler(logging.Handler):
        """
        A logging handler capturing all (raw and formatted) logging output.
        """
    
        def __init__(self):
            logging.Handler.__init__(self)
            self.watcher = _LoggingWatcher([], [])
    
        def flush(self):
            pass
    
        def emit(self, record):
            self.watcher.records.append(record)
            msg = self.format(record)
            self.watcher.output.append(msg)
    
    
    class _AssertLogsContext(_BaseTestCaseContext):
        """A context manager used to implement TestCase.assertLogs()."""
    
        LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
    
        def __init__(self, test_case, logger_name, level):
            _BaseTestCaseContext.__init__(self, test_case)
            self.logger_name = logger_name
            if level:
                self.level = logging._levelNames.get(level, level)
            else:
                self.level = logging.INFO
            self.msg = None
    
        def __enter__(self):
            if isinstance(self.logger_name, logging.Logger):
                logger = self.logger = self.logger_name
            else:
                logger = self.logger = logging.getLogger(self.logger_name)
            formatter = logging.Formatter(self.LOGGING_FORMAT)
            handler = _CapturingHandler()
            handler.setFormatter(formatter)
            self.watcher = handler.watcher
            self.old_handlers = logger.handlers[:]
            self.old_level = logger.level
            self.old_propagate = logger.propagate
            logger.handlers = [handler]
            logger.setLevel(self.level)
            logger.propagate = False
            return handler.watcher
    
        def __exit__(self, exc_type, exc_value, tb):
            self.logger.handlers = self.old_handlers
            self.logger.propagate = self.old_propagate
            self.logger.setLevel(self.old_level)
            if exc_type is not None:
                # let unexpected exceptions pass through
                return False
            if len(self.watcher.records) == 0:
                self._raiseFailure(
                    "no logs of level {} or higher triggered on {}"
                    .format(logging.getLevelName(self.level), self.logger.name))
    
    
    class LogTestCase(unittest.TestCase):
    
        def assertLogs(self, logger=None, level=None):
            """Fail unless a log message of level *level* or higher is emitted
            on *logger_name* or its children.  If omitted, *level* defaults to
            INFO and *logger* defaults to the root logger.
    
            This method must be used as a context manager, and will yield
            a recording object with two attributes: `output` and `records`.
            At the end of the context manager, the `output` attribute will
            be a list of the matching formatted log messages and the
            `records` attribute will be a list of the corresponding LogRecord
            objects.
    
            Example::
    
                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'])
            """
            return _AssertLogsContext(self, logger, level)
    

    Now in your unit-testing modules you can use that class:

    #test_my_module
    from logger_test import LogTestCase
    
    class TestMyModule(LogTestCase):
    
        def test_some_feature(self):
            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'])
    
    0 讨论(0)
  • 2021-01-08 01:11

    In your unit test setup, add a logging handler which buffers records, and remove it during teardown. You can use as a basis a couple of utility classes, TestHandler and Matcher, which are part of the Python test infrastructure. (The link is to Python's default branch, but the classes should be usable in other Python versions). For information on how to use these classes, see this post.

    0 讨论(0)
提交回复
热议问题