【问题标题】:Preventing logging file IO during test execution在测试执行期间防止记录文件 IO
【发布时间】:2018-07-16 10:06:18
【问题描述】:

我想测试一个在初始化时记录日志并将日志保存到本地文件的类。因此,我正在模拟日志记录逻辑,以避免在测试时进行文件 IO。这是代表我如何构建测试的伪代码

class TestClass:
    def test_1(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

    def test_2(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

    def test_3(self, monkeypatch):
        monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')
        assert True

注意monkeypatch.setattr() 是如何在所有方法中复制粘贴的。考虑到:

  • 我们事先知道所有调用方法都需要以相同的方式进行猴子修补,并且
  • 可能会忘记猴子补丁新方法,

我认为猴子补丁应该在类级别进行抽象。我们如何在类级别抽象猴子补丁?我希望解决方案类似于以下内容:

import pytest
class TestClass:
    pytest.monkeypatch.setattr('dotted.path.to.logger', lambda *args: '')

    def test_1(self):
        assert True

    def test_2(self):
        assert True

    def test_3(self):
        assert True

这是配置记录器的地方。

def initialise_logger(session_dir: str):
    """If missing, initialise folder "log" to store .log files. Verbosity:
    CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET."""
    os.makedirs(session_dir, exist_ok=True)
    logging.basicConfig(filename=os.path.join(session_dir, 'session.log'),
                        filemode='a',
                        level=logging.INFO,
                        datefmt='%Y-%m-%d %H:%M:%S',
                        format='|'.join(['(%(threadName)s)',
                                         '%(asctime)s.%(msecs)03d',
                                         '%(levelname)s',
                                         '%(filename)s:%(lineno)d',
                                         '%(message)s']))

    # Adopt NYSE time zone (aka EST aka UTC -0500 aka US/Eastern). Source:
    # https://stackoverflow.com/questions/32402502/how-to-change-the-time-zone-in-python-logging
    logging.Formatter.converter = lambda *args: get_now().timetuple()

    # Set verbosity in console. Verbosity above logging level is ignored.
    console = logging.StreamHandler()
    console.setLevel(logging.ERROR)
    console.setFormatter(logging.Formatter('|'.join(['(%(threadName)s)',
                                                     '%(asctime)s',
                                                     '%(levelname)s',
                                                     '%(filename)s:%(lineno)d',
                                                     '%(message)s'])))
    logger = logging.getLogger()
    logger.addHandler(console)


class TwsApp:
    def __init__(self):
        initialise_logger(<directory>)

【问题讨论】:

  • 请编辑您的帖子以添加配置记录器的代码部分。完成后联系我,我会添加答案。
  • @wim 用代码更新了问题。我最终使用了一个带有 scope='class' 的 python.fixture 并使用 MonkeyPatch() form _pytest.monkeypatch import MonkeyPatch。我已经发布了我的解决方案,但如果它更优雅,我很乐意接受!
  • 你在正确的轨道上,但你并没有真正正确地使用 monkeypatch 夹具(它不应该像那样手动实例化)。我已经添加了答案。

标签: python logging mocking pytest monkeypatching


【解决方案1】:

在实践中,我已将夹具放在/test/conftest.py 中。事实上,pytest 会自动从名为conftest.py 的文件中加载fixture,并且可以在测试会话期间应用到任何模块中。

from _pytest.monkeypatch import MonkeyPatch


@pytest.fixture(scope="class")
def suppress_logger(request):
    """Source: https://github.com/pytest-dev/pytest/issues/363"""
    # BEFORE running the test.
    monkeypatch = MonkeyPatch()
    # Provide dotted path to method or function to be mocked.
    monkeypatch.setattr('twsapp.client.initialise_logger', lambda x: None)
    # DURING the test.
    yield monkeypatch
    # AFTER running the test.
    monkeypatch.undo()



import pytest
@pytest.mark.usefixtures("suppress_logger")
class TestClass:
    def test_1(self):
        assert True

    def test_2(self):
        assert True

    def test_3(self):
        assert True

编辑:我最终在 conftest.py 中使用了以下内容

@pytest.fixture(autouse=True)
def suppress_logger(mocker, request):
    if 'no_suppress_logging' not in request.keywords:
        # If not decorated with: @pytest.mark.no_suppress_logging_error
        mocker.patch('logging.error')
        mocker.patch('logging.warning')
        mocker.patch('logging.debug')
        mocker.patch('logging.info')

【讨论】:

    【解决方案2】:

    更简洁的实现:

    # conftest.py
    import pytest
    
    @pytest.fixture(autouse=True)
    def dont_configure_logging(monkeypatch):
        monkeypatch.setattr('twsapp.client.initialise_logger', lambda x: None)
    

    您不需要用夹具标记单个测试,也不需要注入它,无论如何都会应用。

    如果您需要对记录的记录进行断言,请注入 caplog 夹具。请注意,您不需要配置记录器来进行记录断言 - caplog 固定装置将注入它需要的必要处理程序才能正常工作。如果您想自定义用于测试的日志记录格式,请在pytest.inisetup.cfg[tool:pytest] 部分下进行。

    【讨论】:

    • 我明白你的意思,但我认为可能存在autouse=True 相对于scope=class 次优的情况,所以我倾向于认为你的答案依赖于(太很多)强烈假设我们总是想对所有类进行猴子补丁。我们如何猴子补丁一个类?最后一段信息量很大。
    • @MLguy 理想情况下,您应该在使用 pytest 时摆脱基于类的思维。如果您不想修补所有类,那么您可以使用多个 conftest.py 文件,甚至直接在 .py 模块中定义夹具,只包含需要它的测试。当模块/文件系统已经提供了一些合理的结构时,不再需要也不再需要将测试分组到类中。
    • 使用类提高了可读性,因为它们允许根据测试的类包装测试。所以如果module.py 定义类AB,那么test_module.y 将定义TestATestB。如果module.py 定义了一个类,那么我可能会同意测试类是多余的。请参阅我的答案中的 EDIT 以查看我目前正在使用的内容(受到您的答案的启发,具有很强的可读性)。
    • 我看到了您的编辑,但我仍然认为它过于复杂。没有必要在测试期间修补记录器调用(logger.error、logger.warning、logger.debug、logger.info)。您只需要修补日志记录配置。 Pytest 无论如何都会默认捕获所有日志记录,因此抑制记录器调用是多余的。使用测试类可能只是一个偏好问题(我认为它是老式的 xunit 样式,而在 pytest 中它只是添加了一层额外的不必要的嵌套,但意见不同)。
    猜你喜欢
    • 2015-06-03
    • 1970-01-01
    • 2018-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-27
    • 2023-01-20
    • 1970-01-01
    相关资源
    最近更新 更多