Python 3单元测试 - Assert Logger未调用

时间:2016-03-08 15:47:17

标签: python unit-testing python-3.x logging python-unittest

我知道如何断言生成了日志消息,但我似乎无法弄清楚如何断言没有生成日志消息。这是我现在进行的单元测试(消毒)。请注意,XYZ类将记录器作为参数, test_check_unexpected_keys_found 按预期传递。

import unittest
import logging

class TestXYZ(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.test_logger = logging.getLogger('test_logger')
        cls.test_logger.addHandler(logging.NullHandler())

    def test_check_unexpected_keys_found(self):
        test_dict = {
            'unexpected': 0,
            'expected1': 1,
            'expected2': 2,
        }
        xyz = XYZ(self.test_logger)
        with self.assertLogs('test_logger', level='WARNING'):
            xyz._check_unexpected_keys(test_dict)

    def test_check_unexpected_keys_none(self):
        test_dict = {
            'expected1': 1,
            'expected2': 2,
        }
        xyz = XYZ(self.test_logger)
        xyz._check_unexpected_keys(test_dict)
        # assert that 'test_logger' was not called ??

我尝试使用unittest.patch,如下所示:

with patch('TestXYZ.test_logger.warning') as mock_logwarn:
    xyz._check_unexpected_keys(test_dict)
    self.assertFalse(mock_logwarn.called)

但是我得到了

ImportError: No module named 'TestXYZ'
我也尝试了一些变种,但无处可去。

任何人都知道如何处理这个问题?

2 个答案:

答案 0 :(得分:4)

新的assertNoLogs方法是on route to be added to a future version of Python
在此之前,这里是一种解决方法:添加一个虚拟日志,然后断言它是唯一的日志。

with self.assertLogs(logger, logging.WARN) as cm:
    # We want to assert there are no warnings, but the 'assertLogs' method does not support that.
    # Therefore, we are adding a dummy warning, and then we will assert it is the only warning.
    logger.warn("Dummy warning")
    # DO STUFF

self.assertEqual(
    ["Dummy warning"],
    cm.output,
)

如果您需要多次执行此操作,则为避免重复,可以执行以下操作。假设您有一个继承所有测试类的基类,请按如下所示重写该类中的assertLogs

class TestBase(TestCase):
    def assertLogs(self, logger_to_watch=None, level=None) -> 'CustomAssertLogsContext':
        """
        This method overrides the one in `unittest.case.TestCase`, and has the same behavior, except for not causing a failure when there are no log messages.
        The point is to allow asserting there are no logs.
        Get rid of this once this is resolved: https://github.com/python/cpython/pull/18067
        """
        return CustomAssertLogsContext(self, logger_to_watch, level)

class CustomAssertLogsContext(_AssertLogsContext):    
    def __exit__(self, exc_type, exc_val, exc_tb) -> Optional[bool]:
        # Fool the original exit method to think there is at least one record, to avoid causing a failure
        self.watcher.records.append("DUMMY")
        result = super().__exit__(exc_type, exc_val, exc_tb)
        self.watcher.records.pop()
        return result

答案 1 :(得分:1)

基于Joe的回答,这是一个assertNoLogs(...)例程的实现,作为mixin类,可以在Python 3.10中发布正式版本之前使用它:

import logging
import unittest

def assertNoLogs(self, logger, level):
    """ functions as a context manager.  To be introduced in python 3.10
    """

    class AssertNoLogsContext(unittest.TestCase):
        def __init__(self, logger, level):
            self.logger = logger
            self.level = level
            self.context = self.assertLogs(logger, level)

        def __enter__(self):
            """ enter self.assertLogs as context manager, and log something
            """
            self.initial_logmsg = "sole message"
            self.cm = self.context.__enter__()
            self.logger.log(self.level, self.initial_logmsg)
            return self.cm

        def __exit__(self, exc_type, exc_val, exc_tb):
            """ cleanup logs, and then check nothing extra was logged """
            # assertLogs.__exit__ should never fail because of initial msg
            self.context.__exit__(exc_type, exc_val, exc_tb)
            if len(self.cm.output) > 1:
                """ override any exception passed to __exit__ """
                self.context._raiseFailure(
                    "logs of level {} or higher triggered on : {}"
                    .format(logging.getLevelName(self.level),
                            self.logger.name, self.cm.output[1:]))

    return AssertNoLogsContext(logger, level)

要使用它,只需使用以下命令开始测试用例

class Testxxx(unittest.TestCase, AssertNoLog):
    ...

以下测试案例展示了其工作原理:

import unittest
import logging
class TestAssertNL(unittest.TestCase, AssertNoLog):

    def test_assert_no_logs(self):
        """ check it works"""
        log = logging.getLogger()
        with self.assertNoLogs(log, logging.INFO):
            _a = 1
            log.debug("not an info message")

    @unittest.expectedFailure
    def test2_assert_no_logs(self):
        """ check it records failures """
        log = logging.getLogger()
        with self.assertNoLogs(log, logging.INFO):
            _a = 1
            log.info("an info message")

    def test3_assert_no_logs_exception_handling(self):
        log = logging.getLogger()
        with self.assertRaises(TypeError):
            with self.assertNoLogs(log, logging.INFO):
                raise TypeError('this is not unexpected')

    def test4_assert_no_logs_exception_handling(self):
        """ the exception gets reported as the failure.
        This matches the behaviour of assertLogs(...) """
        log = logging.getLogger()
        with self.assertRaises(AssertionError):
            with self.assertNoLogs(log, logging.INFO):
                log.info("an info message")
                raise TypeError('this is not unexpected')