【问题标题】:How to fake a module in unittest?如何在单元测试中伪造模块?
【发布时间】:2020-10-08 16:03:37
【问题描述】:

我有一个基于名为config.py 的配置文件的代码,该文件定义了一个名为Config 的类并包含所有配置选项。由于配置文件可以位于用户存储中的任何位置,所以我使用importlib.util 来导入它(在此answer 中指定)。我想用unittest 测试这个功能以进行不同的配置。我该怎么做?一个简单的答案可能是为我想要测试的每个可能的配置创建一个不同的文件,然后将其路径传递给配置加载器,但这不是我想要的。我基本上需要的是实现Config 类,并将其伪装成实际的配置文件。如何做到这一点?

编辑这是我要测试的代码:

import os
import re
import traceback
import importlib.util
from typing import Any
from blessings import Terminal

term = Terminal()


class UnknownOption(Exception):
    pass


class MissingOption(Exception):
    pass


def perform_checks(config: Any):
    checklist = {
        "required": {
            "root": [
                "flask",
                "react",
                "mysql",
                "MODE",
                "RUN_REACT_IN_DEVELOPMENT",
                "RUN_FLASK_IN_DEVELOPMENT",
            ],
            "flask": ["HOST", "PORT", "config"],
             # More options
        },
        "optional": {
            "react": [
                "HTTPS",
                # More options
            ],
            "mysql": ["AUTH_PLUGIN"],
        },
    }

    # Check for missing required options
    for kind in checklist["required"]:
        prop = config if kind == "root" else getattr(config, kind)
        for val in kind:
            if not hasattr(prop, val):
                raise MissingOption(
                    "Error while parsing config: "
                    + f"{prop}.{val} is a required config "
                    + "option but is not specified in the configuration file."
                )

    def unknown_option(option: str):
        raise UnknownOption(
            "Error while parsing config: Found an unknown option: " + option
        )

    # Check for unknown options
    for val in vars(config):
        if not re.match("__[a-zA-Z0-9_]*__", val) and not callable(val):
            if val in checklist["optional"]:
                for ch_val in vars(val):
                    if not re.match("__[a-zA-Z0-9_]*__", ch_val) and not callable(
                        ch_val
                    ):
                        if ch_val not in checklist["optional"][val]:
                            unknown_option(f"Config.{val}.{ch_val}")
            else:
                unknown_option(f"Config.{val}")

    # Check for illegal options
    if config.react.HTTPS == "true":

        # HTTPS was set to true but no cert file was specified
        if not hasattr(config.react, "SSL_KEY_FILE") or not hasattr(
            config.react, "SSL_CRT_FILE"
        ):
            raise MissingOption(
                "config.react.HTTPS was set to True without specifying a key file and a crt file, which is illegal"
            )
        else:

            # Files were specified but are non-existent
            if not os.path.exists(config.react.SSL_KEY_FILE):
                raise FileNotFoundError(
                    f"The file at { config.react.SSL_KEY_FILE } was set as the key file"
                    + "in configuration but was not found."
                )
            if not os.path.exists(config.react.SSL_CRT_FILE):
                raise FileNotFoundError(
                    f"The file at { config.react.SSL_CRT_FILE } was set as the certificate file"
                    + "in configuration but was not found."
                )


def load_from_pyfile(root: str = None):
    """
    This loads the configuration from a `config.py` file located in the project root
    """
    PROJECT_ROOT = root or os.path.abspath(
        ".." if os.path.abspath(".").split("/")[-1] == "lib" else "."
    )
    config_file = os.path.join(PROJECT_ROOT, "config.py")

    print(f"Loading config from {term.green(config_file)}")

    # Load the config file
    spec = importlib.util.spec_from_file_location("", config_file)
    config = importlib.util.module_from_spec(spec)

    # Execute the script
    spec.loader.exec_module(config)

    # Not needed anymore
    del spec, config_file

    # Load the mode from environment variable and
    # if it is not specified use development mode
    MODE = int(os.environ.get("PROJECT_MODE", -1))
    conf: Any

    try:
        conf = config.Config()
        conf.load(PROJECT_ROOT, MODE)
    except Exception:
        print(term.red("Fatal: There was an error while parsing the config.py file:"))
        traceback.print_exc()
        print("This error is non-recoverable. Aborting...")
        exit(1)

    print("Validating configuration...")
    perform_checks(conf)
    print(
        "Configuration",
        term.green("OK"),
    )

【问题讨论】:

    标签: python unit-testing module python-unittest python-importlib


    【解决方案1】:

    如果没有看到更多代码,很难给出非常直接的答案,但很可能你想使用Mocks

    在单元测试中,您将使用模拟来替换该类的调用者/消费者的 Config 类。然后,您将模拟配置为提供与您的测试用例相关的返回值或副作用。

    根据您发布的内容,您可能不需要任何模拟,只需要固定装置。也就是说,练习给定案例的Config 的示例。事实上,最好完全按照您最初的建议进行操作——只需制作一些示例配置来练习所有重要的案例。 目前尚不清楚为什么这是不可取的——根据我的经验,阅读和理解具有连贯夹具的测试比在测试类中处理模拟和构造对象要容易得多。此外,如果您将 perform_checks 函数分解为多个部分(例如,您有 cmets 的位置),您会发现这更容易测试。

    但是,您可以根据需要构造 Config 对象并将它们传递给单元测试中的检查函数。使用 dict 固定装置是 Python 开发中的一种常见模式。记住在 python 对象中,包括模块,有一个很像字典的接口,假设你有一个单元测试

    from unittest import TestCase
    from your_code import perform_checks
    
    class TestConfig(TestCase):
      def test_perform_checks(self): 
        dummy_callable = lambda x: x
        config_fixture = {
           'key1': 'string_val',
           'key2': ['string_in_list', 'other_string_in_list'],
           'key3': { 'sub_key': 'nested_val_string', 'callable_key': dummy_callable}, 
           # this is your in-place fixture
           # you make the keys and values that correspond to the feature of the Config file under test.
    
        }
        perform_checks(config_fixture)
        self.assertTrue(True) # i would suggest returning True on the function instead, but this will cover the happy path case
        
      def perform_checks_invalid(self):
        config_fixture = {}
        with self.assertRaises(MissingOption):
           perform_checks(config_fixture)
    
    # more tests of more cases
    

    如果您想在测试之间共享固定装置,您还可以覆盖 unittest 类的 setUp() 方法。一种方法是设置一个有效的夹具,然后在每个测试方法中进行您想要测试的无效更改。

    【讨论】:

    • 好的,我已经编辑了帖子以包含代码。你能把答案说得更具体一点吗?
    • 如果您想使用模拟来理解,那么重要的代码是单元测试。
    • 那是我正在寻找的代码,因为那是我无法理解的部分
    • 是的,我明白了。也就是说,这个网站的前提是你发布你的最大努力并解释你尝试了什么。这样就更有可能有人可以帮助您,也更有可能有人帮助您。
    猜你喜欢
    • 1970-01-01
    • 2017-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    • 2023-04-07
    相关资源
    最近更新 更多