Як мені перевірити повідомлення журналу при тестуванні коду Python під носом?


80

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

Я знаю, що nose вже фіксує вихідні дані журналу через плагін реєстрації, але, схоже, це призначено як допоміжний засіб звітування та налагодження для невдалих тестів.

Я бачу два способи зробити це:

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

Якщо я піду на попередній підхід, я хотів би знати, який найчистіший спосіб повернути глобальний стан до стану, який був до того, як я висміяв модуль реєстрації.

З нетерпінням чекаємо ваших підказок та підказок щодо цього ...


2
Зараз для цього є вбудований спосіб: docs.python.org/3/library/…
wkschwartz

Відповіді:


23

Раніше я знущався з реєстраторів, але в цій ситуації я вважав, що найкраще використовувати обробники журналів, тому написав цей на основі документа, запропонованого 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': [],
        }

1
Вищевказане посилання мертве, і мені було цікаво, чи може хтось написати про те, як використовувати цей код. Коли я намагаюся додати цей обробник журналу, я постійно отримую повідомлення про помилку, намагаючись використовувати його як AttributeError: class MockLoggingHandler has no attribute 'level'.
Ренді

131

З пітона 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'])

38

На щастя, це не те, що вам доводиться писати самому; testfixturesпакет надає менеджер контексту , який фіксує всі вихідні дані протоколювання , що відбувається в тілі withзаяви. Ви можете знайти пакет тут:

http://pypi.python.org/pypi/testfixture

І ось його документи про тестування реєстрації:

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


2
Це рішення не тільки виглядало більш елегантним, оскільки насправді працювало для мене, тоді як інші - ні (мій журнал походить із декількох потоків).
Paulo SantAnna

33

ОНОВЛЕННЯ : Більше не потрібно відповідати нижче. Натомість використовуйте вбудований спосіб Python !

Ця відповідь продовжує роботу, виконану в https://stackoverflow.com/a/1049375/1286628 . Обробник в основному однаковий (конструктор більш ідіоматичний, використовуючи super). Далі я додаю демонстрацію того, як використовувати обробник зі стандартними бібліотеками 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 (e.g., '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."
        try:
            self.messages[record.levelname.lower()].append(record.getMessage())
        except Exception:
            self.handleError(record)

    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)

Дякуємо, що пояснили, як використати відповідь Густаво та розширили її.
Харшдіп

2
Зараз для цього є вбудований спосіб: docs.python.org/3/library/…
wkschwartz

1
У setUpClass у виклику foo_log.addHandler () відсутнє підкреслення перед foo_log_handlerзмінною
PaulR

Це все ще корисно для python 2.x.
jdhildeb

1
Імовірно, будь-який код, написаний на Python 2, який все ще використовується, вже перевірений. Якщо ви перебуваєте на етапі тестового написання проекту, можливо, краще просто перейти на Python 3 зараз. Python 2 втратить підтримку (включаючи оновлення безпеки) приблизно через півтора року.
wkschwartz

13

Відповідь Брендона:

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)

Примітка: вищезазначене не суперечить виклику тестів носа та отриманню вихідних даних плагіна logCapture інструменту


3

Наслідуючи відповідь Рифа, я дозволив собі кодувати приклад, використовуючи 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()

2
Мені подобається ваше інноваційне використання декоратора контекстних менеджерів для реалізації "знущань над знущанням". Приємно.
jkp

PS: це справжня ганьба через відсутність відповідності PyMOX pep8.
jkp

2

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

Знущання продовжуватиме працювати, навіть якщо стандартний вихід буде придушений.

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


+1 Деякі переваги AOP. Замість того, щоб обертати кожну серверну частину у загальному стилі класу / об’єкта.
Aiden Bell


1

Якщо ви визначите допоміжний метод наступним чином:

import logging

def capture_logging():
    records = []

    class CaptureHandler(logging.Handler):
        def emit(self, record):
            records.append(record)

        def __enter__(self):
            logging.getLogger().addHandler(self)
            return records

        def __exit__(self, exc_type, exc_val, exc_tb):
            logging.getLogger().removeHandler(self)

    return CaptureHandler()

Тоді ви можете написати тестовий код таким чином:

    with capture_logging() as log:
        ... # trigger some logger warnings
    assert len(log) == ...
    assert log[0].getMessage() == ...


0

Виключивши відповідь @ Reef, я спробував код нижче. Для мене це добре працює як для Python 2.7 (якщо ви встановлюєте макет ), так і для 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()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.