Python 在测试执行期间防止日志文件IO

Python 在测试执行期间防止日志文件IO,python,logging,mocking,pytest,monkeypatching,Python,Logging,Mocking,Pytest,Monkeypatching,我想测试一个在初始化时进行日志记录的类,并将日志保存到本地文件。因此,为了避免测试时出现文件IO,我模拟了日志逻辑。这是表示我如何构造测试的伪代码 class TestClass: def test_1(self, monkeypatch): monkeypatch.setattr('dotted.path.to.logger', lambda *args: '') assert True def test_2(self, monkeypatch

我想测试一个在初始化时进行日志记录的类,并将日志保存到本地文件。因此,为了避免测试时出现文件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()。考虑到:

  • 我们事先知道,所有调用方法都需要以相同的方式进行修补,并且
  • 有人可能会忘记修补新方法
我认为猴子补丁应该在类级别抽象。我们如何在类级别抽象monkeypatching?我希望解决方案与以下类似:

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>)
def初始化记录器(会话目录:str):
“”“如果缺少,请初始化文件夹“log”以存储.log文件。详细信息:
严重、错误、警告、信息、调试、未设置。”“”
os.makedirs(session\u dir,exist\u ok=True)
logging.basicConfig(filename=os.path.join(session_dir,'session.log'),
filemode='a',
级别=logging.INFO,
datefmt=“%Y-%m-%d%H:%m:%S”,
格式=“|”。联接([”(((threadName)s)”,
“%(asctime)s.%(毫秒)03d”,
“%(levelname)s”,
“%(文件名)s:%(行号)d”,
“%(消息)s']))
#采用纽约证券交易所时区(又名东部时间又名UTC-0500又名美国/东部时间)。资料来源:
# https://stackoverflow.com/questions/32402502/how-to-change-the-time-zone-in-python-logging
logging.Formatter.converter=lambda*args:get_now().timetuple()
#在控制台中设置详细信息。日志级别以上的详细信息将被忽略。
console=logging.StreamHandler()
console.setLevel(logging.ERROR)
console.setFormatter(logging.Formatter(“|”).join([”((threadName)s),
“%(asctime)s”,
“%(levelname)s”,
“%(文件名)s:%(行号)d”,
“%(消息)s']))
logger=logging.getLogger()
logger.addHandler(控制台)
TwsApp类:
定义初始化(自):
初始化_记录器()

在实践中,我将夹具放在了
/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')

更清洁的实施:

# conftest.py
import pytest

@pytest.fixture(autouse=True)
def dont_configure_logging(monkeypatch):
    monkeypatch.setattr('twsapp.client.initialise_logger', lambda x: None)
您不需要用fixture标记单个测试,也不需要注入它,这将被应用


如果需要对记录进行断言,则注入fixture。请注意,您不需要配置记录器来进行日志断言,
caplog
fixture将注入正确工作所需的必要处理程序。如果要自定义用于测试的日志记录格式,请在
pytest.ini
中或在
setup.cfg
[工具:pytest]
部分下进行

请编辑您的帖子,添加配置记录器的代码部分。完成后请Ping我,我将添加答案。@wim用代码更新了问题。我最终使用了一个带有scope='class'的python.fixture,并使用MonkeyPatch()表单_pytest.MonkeyPatch导入MonkeyPatch。我已经发布了我的解决方案,但如果它更优雅,我很乐意接受你的建议!您的思路是正确的,但是您没有真正正确地使用monkeypatch装置(不应该像那样手动实例化)。我添加了一个答案。我明白你的意思,但我认为可能存在这样的情况,
autouse=True
scope=class
方面是次优的,所以我倾向于认为你的答案依赖于(太多)强烈的假设,即我们总是想要修补所有类。我们如何修补单个类?最后一段内容非常丰富。在理想情况下,在使用pytest时,您应该远离基于类的思维。如果您不想修补所有类,那么您可以使用多个
conftest.py
文件,甚至只需在.py模块中直接使用需要的测试来定义fixture。当模块/文件系统已经提供了一些合理的结构时,将测试分组到类中不再需要,也不再可取。使用类可以提高可读性,因为它们允许根据测试的类包装测试。因此,如果
module.py
定义了类
A
B
,那么
test\u module.y
将定义
TestA
TestB
。如果
module.py
定义了一个类,那么我可能同意测试类是多余的。查看我的答案中的编辑以查看我当前使用的内容(这是受你的答案的启发,可读性很强)。我看到了你的编辑,但我仍然认为这过于复杂了。测试期间不必修补记录器调用(logger.error、logger.warning、logger.debug、logger.info)。您只需要修补日志记录配置。Pytest默认情况下会捕获所有日志记录,因此禁止日志记录调用是多余的。使用测试类可能只是一个偏好问题(我认为这是老式的xunit风格,而在pytest中,它只是添加了一层额外的不必要的嵌套,但观点不同)。