web-dev-qa-db-ja.com

鼻の下でPythonコードをテストするときに、ログメッセージをどのように確認する必要がありますか?

特定の条件下で、アプリケーションのクラスが標準のログAPIを介してエラーをログに記録することを確認する簡単な単体テストを作成しようとしています。私はこの状況をテストする最もクリーンな方法が何であるかを理解することができません。

Noseは既にログ出力プラグインを介してログ出力をキャプチャしていることを知っていますが、これは失敗したテストのレポートとデバッグの補助として意図されているようです。

これを行うには、次の2つの方法があります。

  • ロギングモジュールを少しずつ(mymodule.logging = mockloggingmodule)または適切なモッキングライブラリを使用してモックアウトします。
  • 既存のノーズプラグインを作成または使用して、出力をキャプチャして検証します。

前者のアプローチをとる場合、ロギングモジュールをモックアウトする前の状態にグローバル状態をリセットする最もクリーンな方法を知りたいのですが。

これに関するヒントとヒントを楽しみにしています...

52
jkp

以前はロガーをモック化していましたが、この状況ではロギングハンドラーを使用するのが最善であると考えたため、これを 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': [],
        }
19
Gustavo Narea

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

幸いなことに、これは自分で書く必要があるものではありません。 testfixturesパッケージは、withステートメントの本文で発生するすべてのログ出力をキャプチャするコンテキストマネージャーを提供します。あなたはここでパッケージを見つけることができます:

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

そして、ここにロギングをテストする方法に関するそのドキュメントがあります:

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

34
Brandon Rhodes

[〜#〜] 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)
30
wkschwartz

ブランドンの答え:

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プラグインの出力の取得と競合しません

9

リーフの回答のフォローアップとして、私は 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()
3
Pavel Repin

いつかモックを使用する必要があります。たとえば、ロガーをデータベースに変更したい場合があります。 nosetests中にデータベースに接続しようとすると、満足できません。

標準出力が抑制されてもモッキングは機能し続けます。

pyMox のスタブを使用しました。テスト後にスタブの設定を解除することを忘れないでください。

1
Paweł Polewicz

トルネードに実装されているExpectLogクラスは優れたユーティリティです。

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

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

0
Taha Jahangir

@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()
0
twildfarmer

見つかった 1つの回答 私がこれを投稿してから。悪くない。

0
jkp