Python 2.7 Unit test: Assert logger warning thrown

前端 未结 2 1381
逝去的感伤
逝去的感伤 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'])
    

提交回复
热议问题