96

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

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

私が見ることができるこれを行う2つの方法は次のとおりです。

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

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

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

4

12 に答える 12

39

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

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

そして、ロギングをテストする方法についてのドキュメントは次のとおりです。

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

于 2013-02-02T20:36:27.420 に答える
36

更新:以下の回答は不要になりました。代わりに、組み込みの Python の方法を使用してください。

この回答は、 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)
于 2013-12-12T20:15:41.307 に答える
30

以前はロガーのモックを作成していましたが、この状況ではロギング ハンドラーを使用するのが最適であることがわかったため、jkp によって提案されたドキュメントに基づいてこれを作成しました (現在は無効ですが、インターネット アーカイブにキャッシュされています) 。

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': [],
        }
于 2009-06-26T14:13:44.763 に答える
15

ブランドンの答え:

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

于 2015-02-03T18:33:46.227 に答える
11

すべての中で最も簡単な答え

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 フィクスチャには多くの利点があります。

于 2021-02-18T23:01:07.733 に答える
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()
于 2009-05-22T22:57:26.970 に答える
2

いつかあなたのロガーを例えばデータベースのロガーに変更したくなるかもしれないので、モッキングを使うべきです。ノーズテスト中にデータベースに接続しようとすると、満足できません。

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

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

于 2009-05-22T17:52:39.577 に答える
1

これを投稿してから、1つの答えが見つかりました。悪くない。

于 2009-05-22T17:52:55.503 に答える
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()
于 2016-01-12T01:54:46.633 に答える
0

ExpectLogtornado で実装されたクラスは優れたユーティリティです。

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

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

于 2014-12-03T13:03:35.827 に答える