特定の条件下で、アプリケーションのクラスが標準のログAPIを介してエラーをログに記録することを確認する簡単な単体テストを作成しようとしています。私はこの状況をテストする最もクリーンな方法が何であるかを理解することができません。
Noseは既にログ出力プラグインを介してログ出力をキャプチャしていることを知っていますが、これは失敗したテストのレポートとデバッグの補助として意図されているようです。
これを行うには、次の2つの方法があります。
前者のアプローチをとる場合、ロギングモジュールをモックアウトする前の状態にグローバル状態をリセットする最もクリーンな方法を知りたいのですが。
これに関するヒントとヒントを楽しみにしています...
以前はロガーをモック化していましたが、この状況ではロギングハンドラーを使用するのが最善であると考えたため、これを 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': [],
}
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'])
幸いなことに、これは自分で書く必要があるものではありません。 testfixtures
パッケージは、with
ステートメントの本文で発生するすべてのログ出力をキャプチャするコンテキストマネージャーを提供します。あなたはここでパッケージを見つけることができます:
http://pypi.python.org/pypi/testfixtures
そして、ここにロギングをテストする方法に関するそのドキュメントがあります:
[〜#〜] update [〜#〜]:以下の答えは不要になりました。代わりに 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)
ブランドンの答え:
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 __== '__main__':
unittest.main()
いつかモックを使用する必要があります。たとえば、ロガーをデータベースに変更したい場合があります。 nosetests中にデータベースに接続しようとすると、満足できません。
標準出力が抑制されてもモッキングは機能し続けます。
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 __== '__main__':
unittest.main()
見つかった 1つの回答 私がこれを投稿してから。悪くない。