【问题标题】:Mock context manager inside a class类中的模拟上下文管理器
【发布时间】:2022-01-22 05:58:53
【问题描述】:

我在一个类方法中有以下上下文管理器,我想模拟它以进行单元测试。

def load_yaml_config(self) -> dict:
    """
    Load the config based on the arguments provided.

    Returns: dict
        dictionary which will be used for configuring the logger, handlers, etc.
    """

    with open(self.yaml_path, 'r') as config_yaml:
        return yaml.safe_load(config_yaml.read())

我怎样才能实现它?

编辑:

正如@chepner 所建议的(我不能接受他/她的回答,因为它是通过评论),最好的方法似乎是使用 unittest 的 mock_open 功能。

这样,我可以简单地走了:

import unittest.mock as um

with um.patch('builtins.open', um.mock_open(read_data=YAML_TEST)):
    h = MyClass.load_yaml_config()

【问题讨论】:

  • 两个选项:补丁open,或者写一个带有测试数据的临时文件并将yaml_path指向它。我个人可能会选择后者,因为将测试数据写入文件比模拟文件对象更容易。
  • @Samwise 您能否将其发布在答案中,以便我更容易理解这个概念?
  • 这是您的 IDE 发出警告,而不是 Python 本身。你可以忽略它,或者使用docs.python.org/3/library/unittest.mock.html#mock-open
  • 您希望测试实际测试什么?您的 mocked_yaml 测试函数甚至不会调用您的 load_yaml_config 函数。您是否要测试load_yaml_configyaml.safe_load,两者都不测试...?
  • 我只想测试 load_yaml_config。 @Samwise

标签: python unit-testing mocking contextmanager


【解决方案1】:

如果您想重构此代码以便能够测试 safe_load 部分而无需实际 open 文件修补 builtins.open,您可以这样做:

def load_yaml_config(self) -> dict:
    """
    Load the config based on the arguments provided.

    Returns: dict
        dictionary which will be used for configuring the logger, handlers, etc.
    """

    with open(self.yaml_path, 'r') as config_yaml:
        return self._load_yaml_config(config_yaml.read())

def _load_yaml_config(self, yaml_text: str) -> dict:
    return yaml.safe_load(yaml_text)

然后在你的测试中:

TEST_YAML_DATA = """
stuff:
  other_stuff
"""

def test_load_yaml_config():
    assert WhateverMyClassIs()._load_yaml_config(TEST_YAML_DATA) == {
        'stuff': 'other_stuff'
    }

修改为使用实际适当的 YAML 格式和正确的预期 dict 输出。

请注意,所有这些真正的测试都是yaml.safe_load(应该已经有自己的单元测试)以及您的代码调用它的事实。除了变量名中的拼写错误(使用 linter 或静态类型分析器更容易捕获)之外,很难想象这个测试可能会捕获/防止什么类型的错误。

实际上,我可能根本不会费心在单元测试中介绍此功能,而是会尝试进行某种更大的集成测试(使用真实文件),其中涉及加载配置作为更大测试的一部分场景。

【讨论】:

    【解决方案2】:

    简单的方法

    只需在您的测试代码中创建适当的 yaml 文件即可。但你可能不希望这样,因为你正在发这篇文章。

    嘲讽的技巧

    您可以在模块范围内使用您的模拟覆盖open

    # test_YourClass.py
    
    builtin_open = open
    
    class open:
        def __init__(self, *args, **kwargs):
            pass
        def __enter__(self):
            pass
        def __exit__(self, exc_type, exc_value, exc_traceback):
            pass
        def read(self):
            return 'hardcoded file contents for testing'
    
    # Test here
    
    open = builtin_open
    

    这段代码只是一个大概的想法,我没有运行它。它可能需要一些额外的工作,例如参数化模拟文件内容。

    依赖注入

    我想,“正确”的方法是在类中对 open() 调用进行非硬编码并注入您的上下文管理器。由你决定。我个人不喜欢仅仅为了单元测试的目的而注入所有东西。

    【讨论】:

    • 基本上,您只是拒绝自己打开文件的责任。 load_yaml_config 将类文件对象作为 参数,并对其进行解析。调用者负责提供该对象,无论是通过创建StringIO 对象,还是使用with open(obj.yaml_path) as f: obj.load_yaml_config(f),或其他选项。
    • 'r''rt',真的)是默认模式,所以可以省略。我唯一一次明确使用它是在以二进制模式打开文件时,例如open(binfile, 'rb')
    • @banana_99 我从上面 chepner 的评论中获取了obj。这是您帖子中的类的一个实例(具有yaml_pathload_yaml_config() 的类)
    • 很确定您想在 open 函数上使用 mock.patch 而不仅仅是分配给它。
    • 但是 +++ 用于“在您的测试代码中创建适当的 yaml 文件”。这是迄今为止更简单的方法,也是测试代码是否按预期工作的更好方法。
    最近更新 更多