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:
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 ...
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': [],
}
À 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'])
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:
[~ # ~] 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)
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
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()
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.
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
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()
Trouvé ne réponse depuis que j'ai posté ceci. Pas mal.