Как проверить сообщение журнала при тестировании кода Python под носом?

Я пытаюсь написать простой модульный тест, который будет проверять, что при определенном условии класс в моем приложении будет регистрировать ошибку с помощью стандартного API протоколирования. Я не могу понять, какой самый чистый способ проверить эту ситуацию.

Я знаю, что нос уже фиксирует вывод журнала через его плагин регистрации, но это, по-видимому, предназначено как средство отчетности и отладки для неудачных тестов.

Два способа сделать это я вижу:

  • Выделите модуль каротажа либо по частям (mymodule.logging = mockloggingmodule), либо с помощью надлежащей насмешливой библиотеки.
  • Напишите или используйте существующий плагин для носа, чтобы захватить вывод и проверить его.

Если я пойду на прежний подход, я хотел бы знать, какой самый чистый способ сбросить глобальное состояние до того, что было до того, как я высмеял модуль регистрации.

С нетерпением ждем ваших намеков и советов по этому …

10 Solutions collect form web for “Как проверить сообщение журнала при тестировании кода Python под носом?”

Я использовал для издевки регистраторов, но в этой ситуации я нашел лучший способ использовать обработчики протоколирования, поэтому я написал это на основе документа, предложенного jkp :

class MockLoggingHandler(logging.Handler): """Mock logging handler to check for expected logs.""" def __init__(self, *args, **kwargs): self.reset() logging.Handler.__init__(self, *args, **kwargs) def emit(self, record): self.messages[record.levelname.lower()].append(record.getMessage()) def reset(self): self.messages = { 'debug': [], 'info': [], 'warning': [], 'error': [], 'critical': [], } 

К счастью, это не то, что вы должны написать себе; пакет testfixtures предоставляет менеджер контекста, который фиксирует весь вывод журнала, который встречается в теле оператора. Пакет можно найти здесь:

http://pypi.python.org/pypi/testfixtures

И вот его документы о том, как тестировать протоколирование:

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

Начиная с версии python 3.4, стандартная библиотека unittest предлагает новый менеджер контекста утверждения, assertLogs . Из документов :

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

Этот ответ расширяет работу, проделанную в https://stackoverflow.com/a/1049375/1286628 . Обработчик в основном одинаков (конструктор более идиоматичен, используя super , и emit теперь является потокобезопасным для тестирования асинхронного кода, такого как задачи Celery). Кроме того, я добавляю демонстрацию того, как использовать обработчик с unittest стандартной библиотеки.

 class MockLoggingHandler(logging.Handler): """Mock logging handler to check for expected logs. Messages are available from an instance's ``messages`` dict, in order, indexed by a lowercase log level string (eg, 'debug', 'info', etc.). """ def __init__(self, *args, **kwargs): self.messages = {'debug': [], 'info': [], 'warning': [], 'error': [], 'critical': []} super(MockLoggingHandler, self).__init__(*args, **kwargs) def emit(self, record): "Store a message from ``record`` in the instance's ``messages`` dict." self.acquire() try: self.messages[record.levelname.lower()].append(record.getMessage()) finally: self.release() def reset(self): self.acquire() try: for message_list in self.messages.values(): message_list.clear() finally: self.release() 

Затем вы можете использовать обработчик в стандартной библиотеке unittest.TestCase например:

 import unittest import logging import foo class TestFoo(unittest.TestCase): @classmethod def setUpClass(cls): super(TestFoo, cls).setUpClass() # Assuming you follow Python's logging module's documentation's # recommendation about naming your module's logs after the module's # __name__,the following getLogger call should fetch the same logger # you use in the foo module foo_log = logging.getLogger(foo.__name__) cls._foo_log_handler = MockLoggingHandler(level='DEBUG') foo_log.addHandler(cls.foo_log_handler) cls.foo_log_messages = cls._foo_log_handler.messages def setUp(self): super(TestFoo, self).setUp() self._foo_log_handler.reset() # So each test is independent def test_foo_objects_fromble_nicely(self): # Do a bunch of frombling with foo objects # Now check that they've logged 5 frombling messages at the INFO level self.assertEqual(len(self.foo_log_messages['info']), 5) for info_message in self.foo_log_messages['info']: self.assertIn('fromble', info_message) 

Ответ Брэндона:

 pip install testfixtures 

фрагмент кода:

 import logging from testfixtures import LogCapture logger = logging.getLogger('') with LogCapture() as logs: # my awesome code logger.error('My code logged an error') assert 'My code logged an error' in str(logs) 

Примечание: вышеупомянутое не конфликтует с вызовом nosetests и получением вывода плагина logCapture инструмента

В ответ на ответ Рифа я взял на себя право кодировать пример с помощью pymox . Он вводит некоторые дополнительные вспомогательные функции, которые упрощают функции и методы заглушки.

 import logging # Code under test: class Server(object): def __init__(self): self._payload_count = 0 def do_costly_work(self, payload): # resource intensive logic elided... pass def process(self, payload): self.do_costly_work(payload) self._payload_count += 1 logging.info("processed payload: %s", payload) logging.debug("payloads served: %d", self._payload_count) # Here are some helper functions # that are useful if you do a lot # of pymox-y work. import mox import inspect import contextlib import unittest def stub_all(self, *targets): for target in targets: if inspect.isfunction(target): module = inspect.getmodule(target) self.StubOutWithMock(module, target.__name__) elif inspect.ismethod(target): self.StubOutWithMock(target.im_self or target.im_class, target.__name__) else: raise NotImplementedError("I don't know how to stub %s" % repr(target)) # Monkey-patch Mox class with our helper 'StubAll' method. # Yucky pymox naming convention observed. setattr(mox.Mox, 'StubAll', stub_all) @contextlib.contextmanager def mocking(): mocks = mox.Mox() try: yield mocks finally: mocks.UnsetStubs() # Important! mocks.VerifyAll() # The test case example: class ServerTests(unittest.TestCase): def test_logging(self): s = Server() with mocking() as m: m.StubAll(s.do_costly_work, logging.info, logging.debug) # expectations s.do_costly_work(mox.IgnoreArg()) # don't care, we test logging here. logging.info("processed payload: %s", 'hello') logging.debug("payloads served: %d", 1) # verified execution m.ReplayAll() s.process('hello') if __name__ == '__main__': unittest.main() 

Вы должны использовать насмешку, так как когда-нибудь вы, возможно, захотите изменить свой регистратор на, скажем, на базу данных. Вы не будете счастливы, если он попытается подключиться к базе данных во время nosetests.

Mocking будет продолжать работать, даже если стандартный вывод будет подавлен.

Я использовал заглушки pyMox . Не забудьте отменить заглушки после теста.

Нашел один ответ, так как я опубликовал это. Неплохо.

Класс ExpectLog реализованный в торнадо, является большой полезностью:

 with ExpectLog('channel', 'message regex'): do_it() 

http://tornado.readthedocs.org/en/latest/_modules/tornado/testing.html#ExpectLog

Отправляя ответ @ Reef, я пробовал код ниже. Это хорошо работает для меня как для Python 2.7 (если вы устанавливаете mock ), так и для Python 3.4.

 """ Demo using a mock to test logging output. """ import logging try: import unittest except ImportError: import unittest2 as unittest try: # Python >= 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch logging.basicConfig() LOG=logging.getLogger("(logger under test)") class TestLoggingOutput(unittest.TestCase): """ Demo using Mock to test logging INPUT. That is, it tests what parameters were used to invoke the logging method, while still allowing actual logger to execute normally. """ def test_logger_log(self): """Check for Logger.log call.""" original_logger = LOG patched_log = patch('__main__.LOG.log', side_effect=original_logger.log).start() log_msg = 'My log msg.' level = logging.ERROR LOG.log(level, log_msg) # call_args is a tuple of positional and kwargs of the last call # to the mocked function. # Also consider using call_args_list # See: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args expected = (level, log_msg) self.assertEqual(expected, patched_log.call_args[0]) if __name__ == '__main__': unittest.main() 
  • Создание и импорт вспомогательных функций в тестах без создания пакетов в тестовой директории с использованием py.test
  • Использование mock для исправления задачи сельдерея в модульных тестах Django
  • Запуск определенных тестов Django (с django-носом?)
  • Не удалось найти элемент поиска по ссылке
  • испытание на фляжку: как проверить запрос от зарегистрированного пользователя
  • Получение unittest Python приводит к методу tearDown ()
  • python mocking raw input в unittests
  • Методы без тестирования в тестовой среде Python
  • Python - лучший язык программирования в мире.