一旦monkeypatch为singleton类设置了函数的init值,则无法覆盖该值

2024-05-26 20:45:55 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个非常简单的python类和一个构造函数:

from utils.util import Singleton
class VaultAuth(object):
    __metaclass__ = Singleton
    def __init__(self, prefix_path, address):     

        self.path = prefix_path
        self.vault_url = address            
        self.is_authenticated = False

    def get_secrets(self, region):         
        print self.is_authenticated 
        if not self.is_authenticated:
            raise RuntimeError("Failed to fetch secrets")
        else:
            return True

其中Singleton类如下所示:

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

要编写单元测试,我有:

@pytest.mark.unit_test
def test_get_secrets(monkeypatch):
    def mock_init_auth_false(self, *args, **kwargs):
        self.path = "dummy_path"
        self.vault_url = "dummy_url"
        self.is_authenticated = False

    def mock_init_auth_true(self, *args, **kwargs):
        self.path = "dummy_path"
        self.vault_url = "dummy_url"
        self.is_authenticated = True

    # Negative case - auth is false
    monkeypatch.setattr(vault1.VaultAuth, "__init__", mock_init_auth_false)
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    with pytest.raises(RuntimeError) as exception:
        secret_data = secrets_manager.get_secrets(region="test_region")
    assert "Failed to fetch secrets" in str(exception.value)
    monkeypatch.undo()

    # Positive case - auth is true
    monkeypatch.setattr(vault1.VaultAuth, "__init__", mock_init_auth_true)
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    assert secrets_manager.get_secrets(region="test_region")   

根据预期,第一个测试打印的值为False,但第二个测试也打印为False。如果我颠倒测试的顺序,两个都打印True。有什么建议吗?这个班是单身汉。如何测试单例类函数?你知道吗


Tags: pathselfauthurlprefixinitisdef
2条回答

好吧,您使用的是Singleton模式,所以只有一个类的实例,__init__方法在这里不会被调用两次。你知道吗

因为您正在单元测试get_secrets()方法,所以只需调整测试的singleton实例的状态:

@pytest.mark.unit_test
def test_get_secrets():
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")

    # Negative case - auth is false
    secrets_manager.is_authenticated = False
    with pytest.raises(RuntimeError) as exception:
        secret_data = secrets_manager.get_secrets(region="test_region")
    assert "Failed to fetch secrets" in str(exception.value)

    # Positive case - auth is true
    secrets_manager.is_authenticated = True
    assert secrets_manager.get_secrets(region="test_region")

但是,上面的测试很容易出现其他问题,因为Singleton类是为Python进程的生存期创建的。如果其他测试使用vault1.VaultAuth(...)任何具有不同参数的地方,那么这些测试就会有问题,因为它们被赋予相同的单例实例。你知道吗

您当然可以在测试中使用singleton类的escape hatch方法;Singleton._clear_singleton()方法将删除给定类的缓存实例,只需调用ClassObject._clear_singleton()

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

    def _clear_singleton(cls):
        # remove the singleton instance for this class (if it exists)
        cls._instances.pop(cls, None)

因此,您可以至少清除现有的单例实例:

# clear singleton cache for VaultAuth before creating test instance:
vault1.VaultAuth._clear_singleton()

try:
    # use vault1.VaultAuth in a test
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    # ...
finally:
    vault1.VaultAuth._clear_singleton()

try:
    # ... more tests
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
finally:
    vault1.VaultAuth._clear_singleton()

try: ... finally:模式确保为测试创建的单例至少被清除。您也可以将其作为pytest夹具:

@pytest.fixture
def vaultauth():
    """The vault1.VaultAuth class, without the singleton cache"""
    vault1.VaultAuth._clear_singleton()
    try:
        yield vault1.VaultAuth
    finally:
        vault1.VaultAuth._clear_singleton()

但这只会清除每个测试函数的单例,而不是在测试过程中多次清除。你知道吗

但是,与模仿__init__不同的是,您可以将子类化为类,因为您将使用不同的类。它们分别作为单件缓存:

@pytest.mark.unit_test
def test_get_secrets():
    # Negative case - auth is false
    class VaultAuthNotAuthenticated(vault1.VaultAuth):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.is_authenticated = False

    secrets_manager = VaultAuthNotAuthenticated(prefix_path="prefix", address="https://vault")
    with pytest.raises(RuntimeError) as exception:
        secret_data = secrets_manager.get_secrets(region="test_region")
    assert "Failed to fetch secrets" in str(exception.value)

    # Positive case - auth is true
    class VaultAuthNotAuthenticated(vault1.VaultAuth):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.is_authenticated = True

    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    assert secrets_manager.get_secrets(region="test_region")   

但是如果类采用特定的参数(前缀、地址),则看起来您在这里使用了错误的模式。你知道吗

您可能希望基于参数缓存实例,因此每个(prefix_path, address)对有一个实例:

class VaultAuth(object):
    _vaults = {}

    def __new__(cls, prefix_path, address):
        key = (prefix_path, address)
        if key not in cls._vaults:
            cls._vaults[key] = self = super(cls, VaultAuth).__new__(cls)
            self.prefix_path = prefix_path
            self.address = address
            self.is_authenticated = False
        return cls._vaults[key]

这将为前缀和地址的每个唯一组合创建一个实例。注意,它没有__init__方法!我故意这么做的,您可以使用一个,但是如果从__new__返回,它将在VaultAuth的任何实例或其子类上调用,即使该实例是在cls._vaults映射之前创建的,并且刚刚从cls._vaults映射返回。你知道吗

无论如何,您可以在测试中使用它,只需使用编造的参数,并更改属性以适合您的测试:

@pytest.mark.unit_test
def test_get_secrets():
    # Negative case - auth is false
    not_authenticated = vault1.VaultAuth(prefix_path="not_authenticated", address="https://foo")
    not_authenticated.is_authenticated = False
    with pytest.raises(RuntimeError) as exception:
        secret_data = not_authenticated.get_secrets(region="test_region")
    assert "Failed to fetch secrets" in str(exception.value)

    # Positive case - auth is true
    authenticated = vault1.VaultAuth(prefix_path="authenticated", address="https://bar")
    authenticated.is_authenticated = True
    assert authenticated.get_secrets(region="test_region")

通过使用特定于测试的参数,创建的实例不太可能干扰其他测试,并且可以为不同的地址创建单独的保险库。你知道吗

您是否尝试过在第二个测试用例之前简单地删除singleton实例?你知道吗

@pytest.mark.unit_test
def test_get_secrets(monkeypatch):
    def mock_init_auth_false(self, *args, **kwargs):
        self.path = "dummy_path"
        self.vault_url = "dummy_url"
        self.is_authenticated = False

    def mock_init_auth_true(self, *args, **kwargs):
        self.path = "dummy_path"
        self.vault_url = "dummy_url"
        self.is_authenticated = True

    # Negative case - auth is false
    monkeypatch.setattr(vault1.VaultAuth, "__init__", mock_init_auth_false)
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    with pytest.raises(RuntimeError) as exception:
        secret_data = secrets_manager.get_secrets(region="test_region")
    assert "Failed to fetch secrets" in str(exception.value)
    monkeypatch.undo()

    del Singleton._instances[vault1.VaultAuth]

    # Positive case - auth is true
    monkeypatch.setattr(vault1.VaultAuth, "__init__", mock_init_auth_true)
    secrets_manager = vault1.VaultAuth(prefix_path="prefix", address="https://vault")
    assert secrets_manager.get_secrets(region="test_region")  

相关问题 更多 >

    热门问题