我正在尝试编写一个简单的单元测试,它将验证在某种情况下,我的应用程序中的类将通过标准日志记录API记录错误。我无法弄清楚测试这种情况最干净的方法是什么。
我知道鼻子已经通过它的日志插件捕获了日志输出,但这似乎是作为失败测试的报告和调试辅助工具。
我能看到的两种方法是:
如果我采用前一种方法,我想知道在嘲笑日志模块之前将全局状态重置为最简洁的方法是什么。
期待您对此的提示和提示...
答案 0 :(得分:60)
从python 3.4开始,标准 unittest 库提供了一个新的测试断言上下文管理器assertLogs
。来自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'])
答案 1 :(得分:33)
幸运的是,这不是你必须自己写的东西; testfixtures
包提供了一个上下文管理器,用于捕获with
语句正文中出现的所有日志记录输出。你可以在这里找到包裹:
http://pypi.python.org/pypi/testfixtures
以下是有关如何测试日志记录的文档:
答案 2 :(得分:28)
更新:不再需要以下答案。改为使用built-in Python way!
这个答案扩展了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)
答案 3 :(得分:20)
我曾经模仿记录器,但在这种情况下我发现最好使用日志处理程序,所以我根据the document suggested by jkp编写了这个(现在已经死了,但缓存在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': [],
}
答案 4 :(得分:9)
布兰登的回答:
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插件的输出相冲突
答案 5 :(得分:3)
作为Reef回答的后续内容,我冒昧地使用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()
答案 6 :(得分:2)
Pytest 有一个名为 caplog
的内置装置。无需设置。
def test_foo(foo, caplog, expected_msgs):
foo.bar()
assert [r.msg for r in caplog.records] == expected_msgs
我希望我能在浪费 6 个小时之前了解 caplog。
<块引用>警告 - 它会重置,因此您需要在对 caplog 进行断言的同一测试中执行 SUT 操作。
就我个人而言,我希望我的控制台输出干净,所以我喜欢这样使 log-to-stderr 静音:
from logging import getLogger
from pytest import fixture
@fixture
def logger(caplog):
logger = getLogger()
_ = [logger.removeHandler(h) for h in logger.handlers if h != caplog.handler] # type: ignore
return logger
@fixture
def foo(logger):
return Foo(logger=logger)
@fixture
def expected_msgs():
# return whatever it is you expect from the SUT
def test_foo(foo, caplog, expected_msgs):
foo.bar()
assert [r.msg for r in caplog.records] == expected_msgs
如果您厌倦了可怕的单元测试代码,那么 pytest 固定装置有很多值得喜欢的地方。
答案 7 :(得分:1)
您应该使用模拟,因为有一天您可能想要将您的记录器更改为数据库之一。如果它在测试过程中尝试连接数据库,你将不会感到高兴。
即使标准输出被抑制,模拟仍将继续有效。
我使用了pyMox的存根。记得在测试后取消设置存根。
答案 8 :(得分: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() == ...
答案 9 :(得分:0)
发布one answer,因为我发布了这个。还不错。
答案 10 :(得分:0)
在龙卷风中实施的ExpectLog
类是一个很棒的实用程序:
with ExpectLog('channel', 'message regex'):
do_it()
http://tornado.readthedocs.org/en/latest/_modules/tornado/testing.html#ExpectLog
答案 11 :(得分:0)
关闭@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()