Python - замена переменных класса синглетонов на mocks - PullRequest
0 голосов
/ 30 января 2019

Я столкнулся с некоторыми трудностями при тестировании моего проекта, в основном из-за того, что контроллеры ссылаются на синглтон, произведенный фабрикой.

Простая демонстрация этой проблемы:

databasefactory.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

model.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'

datacontroller.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()

В тот момент, когда unittest импортирует DataBaseController, DataModel импортируется через модуль DataBaseController.Это означает, что обе переменные класса DATA_BASE_CLIENT создаются.Если бы моя фабрика поймала его на работе внутри юнит-теста, это все равно не имело бы значения, поскольку импорт происходит за пределами юнит-теста.

Мой вопрос: есть ли способ смоделировать этот синглтон и заменить его во всем приложении водин раз?

Замена кэшированного экземпляра на фабрике не вариант, так как ссылки в классах указывают на старый объект.

Возможно, это недостаток проекта, позволяющий помещать эти единичные экземпляры как класспеременные на первом месте.Но я бы предпочел получить переменную класса, чем каждый раз вызывать фабрику для синглтона.

1 Ответ

0 голосов
/ 30 января 2019

В вашем случае использования один модуль отвечает за обеспечение синглтона для всего приложения.Поэтому я бы попытался ввести макет в этот модуль , прежде чем он будет использоваться чем-то еще .Проблема в том, что макет не может быть полностью построен до того, как будут объявлены другие классы.Возможный способ состоит в том, чтобы создать синглтон за 2 прохода: первый проход не зависит ни от чего, затем этот минимальный объект используется для создания классов, а затем заполняется его внутренний словарь.Код может быть:

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)

Но будьте осторожны: это предполагает, что процедура теста не загружает model.py или datacontroller.py перед test_simple.py

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...