gpt4 book ai didi

Python - 用模拟替换类变量单例

转载 作者:行者123 更新时间:2023-12-01 08:23:46 30 4
gpt4 key购买 nike

我在对我的项目进行单元测试时遇到了一些困难,主要是由于 Controller 引用了工厂生产的单例。

这个问题的简单演示是:

数据库工厂.py

class DataBaseFactory(object):

# Lets imagine we support a number of databases. The client implementation all gives us a similar interfaces to use
# This is a singleton through the whole application
_database_client = None

@classmethod
def get_database_client(cls):
# type: () -> DataBaseClientInterFace
if not cls._database_client:
cls._database_client = DataBaseClient()
return cls._database_client


class DataBaseClientInterFace(object):

def get(self, key):
# type: (any) -> any
raise NotImplementedError()

def set(self, key, value):
# type: (any, any) -> any
raise NotImplementedError()


class DataBaseClient(DataBaseClientInterFace):

# Mock some real world database - The unittest mocking should be providing another client
_real_world_data = {}

def get(self, key):
return self._real_world_data[key]

def set(self, key, value):
self._real_world_data[key] = value
return value

模型.py

from .databasefactory import DataBaseFactory


class DataModel(object):

# The DataBase type never changes so its a constant
DATA_BASE_CLIENT = DataBaseFactory.get_database_client()

def __init__(self, model_name):
self.model_name = model_name

def save(self):
# type: () -> None
"""
Save the current model into the database
"""
key = self.get_model_key()
data = vars(self)
self.DATA_BASE_CLIENT.set(key, data)

@classmethod
def load(cls):
# type: () -> DataModel
"""
Load the model
"""
key = cls.get_model_key()
data = cls.DATA_BASE_CLIENT.get(key)
return cls(**data)

@staticmethod
def get_model_key():
return 'model_test'

数据 Controller .py

from .databasefactory import DataBaseFactory
from .model import DataModel


class DataBaseController(object):
"""
Does some stuff with the databaase
"""
# Also needs the database client. This is the same instance as on DataModel
DATA_BASE_CLIENT = DataBaseFactory.get_database_client()

_special_key = 'not_model_key'

@staticmethod
def save_a_model():
a_model = DataModel('test')
a_model.save()

@staticmethod
def load_a_model():
a_model = DataModel.load()
return a_model

@classmethod
def get_some_special_key(cls):
return cls.DATA_BASE_CLIENT.get(cls._special_key)

@classmethod
def set_some_special_key(cls):
return cls.DATA_BASE_CLIENT.set(cls._special_key, 1)

最后是单元测试本身:test_simple.py

import unittest
from .databasefactory import DataBaseClientInterFace
from .datacontroller import DataBaseController
from .model import DataModel


class MockedDataBaseClient(DataBaseClientInterFace):

_mocked_data = {DataBaseController._special_key: 2,
DataModel.get_model_key(): {'model_name': 'mocked_test'}}

def get(self, key):
return self._mocked_data[key]

def set(self, key, value):
self._mocked_data[key] = value
return value


class SimpleOne(unittest.TestCase):

def test_controller(self):
"""
I want to mock the singleton instance referenced in both DataBaseController and DataModel
As DataBaseController imports DataModel, both classes have the DATA_BASE_CLIENT attributed instantiated with the factory result
"""
# Initially it'll throw a keyerror
with self.assertRaises(KeyError):
DataBaseController.get_some_special_key()

# Its impossible to just change the DATA_BASE_CLIENT in the DataBaseController as DataModel still points towards the real implementation
# Should not be done as it won't change anything to data model
DataBaseController.DATA_BASE_CLIENT = MockedDataBaseClient()
self.assertEqual(DataBaseController.get_some_special_key(), 2)
# Will fail as the DataModel still uses the real implementation
# I'd like to mock DATA_BASE_CLIENT for both classes without explicitely giving inserting a new class
# The project I'm working on has a number of these constants that make it a real hassle to inject it a new one
# There has to be a better way to tackle this issue
model = DataBaseController.load_a_model()

当单元测试导入DataBaseController时,DataModel就通过DataBaseController模块导入。这意味着两个 DATA_BASE_CLIENT 类变量都已实例化。如果我的工厂要捕获它在单元测试内运行,那仍然没关系,因为导入发生在单元测试之外。

我的问题是:有没有办法模拟这个单例并立即在整个应用程序中替换?

替换工厂上的缓存实例不是一个选项,因为类中的引用指向旧对象。

首先将这些单例实例作为类变量可能是一个设计缺陷。但我宁愿检索一个类变量,也不愿每次为单例调用工厂。

最佳答案

在您的用例中,单个模块负责向整个应用程序提供单例。因此,我会尝试在该模块被其他任何东西使用之前将其注入(inject)该模块。问题是在声明其他类之前无法完全构造模拟。一种可能的方法是分两遍构造单例:第一遍不依赖于任何东西,然后使用最小对象来构造类,然后填充其内部字典。代码可以是:

import unittest
from .databasefactory import DataBaseClientInterFace

class MockedDataBaseClient(DataBaseClientInterFace):

_mocked_data = {} # no dependance outside databasefactory

def get(self, key):
return self._mocked_data[key]

def set(self, key, value):
self._mocked_data[key] = value
return value

# inject the mock into DataBaseFactory
from .databasefactory import DataBaseFactory
DataBaseFactory._database_client = MockedDataBaseClient()

# use the empty mock to construct other classes
from .datacontroller import DataBaseController
from .model import DataModel

# and populate the mock
DataBaseFactory._database_client._mocked_data.update(
{DataBaseController._special_key: 2,
DataModel.get_model_key(): {'model_name': 'mocked_test'}})

class SimpleOne(unittest.TestCase):

def test_controller(self):
"""
I want to mock the singleton instance referenced in both DataBaseController and DataModel
As DataBaseController imports DataModel, both classes have the DATA_BASE_CLIENT attributed instantiated with the factory result
"""
self.assertEqual(DataBaseController.get_some_special_key(), 2)
model = DataBaseController.load_a_model()
self.assertEqual('mocked_test', model.model_name)

但要注意:这假设测试过程不会在 test_simple.py 之前加载 model.py 或 datacontroller.py

关于Python - 用模拟替换类变量单例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54436154/

30 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com