web-dev-qa-db-fra.com

Comment dois-je vérifier un message de journal lors du test du code Python sous le nez?

J'essaie d'écrire un test unitaire simple qui vérifiera que, dans une certaine condition, une classe de mon application enregistrera une erreur via l'API de journalisation standard. Je ne peux pas déterminer quelle est la façon la plus propre de tester cette situation.

Je sais que Nose capture déjà la sortie de journalisation via son plug-in de journalisation, mais cela semble être conçu comme une aide au reporting et au débogage pour les tests ayant échoué.

Je peux voir les deux façons de procéder:

  • Mock out le module de journalisation, soit de manière fragmentaire (mymodule.logging = mockloggingmodule) ou avec une bibliothèque de mocking appropriée.
  • Écrivez ou utilisez un plug-in de nez existant pour capturer la sortie et la vérifier.

Si je choisis l'ancienne approche, j'aimerais savoir quel est le moyen le plus propre de réinitialiser l'état global à ce qu'il était avant de simuler le module de journalisation.

Dans l'attente de vos conseils et astuces sur celui-ci ...

52
jkp

J'avais l'habitude de me moquer des enregistreurs, mais dans cette situation, j'ai trouvé préférable d'utiliser des gestionnaires de journalisation, j'ai donc écrit celui-ci en fonction de le document suggéré par jkp (maintenant mort, mais mis en cache sur Internet Archive )

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': [],
        }
19
Gustavo Narea

À partir de python 3.4 activé, la bibliothèque standard la plus unitaire propose un nouveau gestionnaire de contexte d'assertion de test, assertLogs À partir du docs :

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'])
68
el.atomo

Heureusement, ce n'est pas quelque chose que vous devez écrire vous-même; le package testfixtures fournit un gestionnaire de contexte qui capture toutes les sorties de journalisation qui se produisent dans le corps de l'instruction with. Vous pouvez trouver le package ici:

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

Et voici ses documents sur la façon de tester la journalisation:

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

34
Brandon Rhodes

[~ # ~] mise à jour [~ # ~] : Plus besoin de réponse ci-dessous. Utilisez plutôt intégré Python !

Cette réponse étend le travail effectué dans https://stackoverflow.com/a/1049375/1286628 . Le gestionnaire est en grande partie le même (le constructeur est plus idiomatique, utilisant super). De plus, j'ajoute une démonstration de l'utilisation du gestionnaire avec le unittest de la bibliothèque standard.

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()

Ensuite, vous pouvez utiliser le gestionnaire dans une bibliothèque standard unittest.TestCase ainsi:

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)
30
wkschwartz

Réponse de Brandon:

pip install testfixtures

fragment:

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)

Remarque: ce qui précède n'entre pas en conflit avec l'appel nosetests et l'obtention de la sortie du plugin logCapture de l'outil

9
Yauhen Yakimovich

Pour faire suite à la réponse de Reef, j'ai pris la liberté de coder un exemple en utilisant pymox . Il introduit des fonctions d'assistance supplémentaires qui facilitent le stub des fonctions et des méthodes.

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 __== '__main__':
    unittest.main()
3
Pavel Repin

Vous devriez utiliser la moquerie, car un jour vous voudrez peut-être changer votre enregistreur en un, par exemple, un base de données. Vous ne serez pas satisfait s'il essaiera de se connecter à la base de données pendant nos tests.

La simulation continuera de fonctionner même si la sortie standard sera supprimée.

J'ai utilisé les talons de pyMox . N'oubliez pas de désinstaller les talons après le test.

1
Paweł Polewicz

La classe ExpectLog implémentée dans tornado est un excellent utilitaire:

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

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

0
Taha Jahangir

En saisissant la réponse de @ Reef, j'ai essayé le code ci-dessous. Cela fonctionne bien pour moi à la fois pour Python 2.7 (si vous installez mock ) et pour 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 __== '__main__':
    unittest.main()
0
twildfarmer

Trouvé ne réponse depuis que j'ai posté ceci. Pas mal.

0
jkp