1

I'm facing some difficulties unittest my project, mainly due to the fact that the controllers reference a singleton produced by a factory.

A simple demonstration of this problem would be:

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)

And finally the unittest itself: 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()

The moment the unittest imports the DataBaseController, DataModel is imported through the DataBaseController module. This means that both DATA_BASE_CLIENT class variables are instantiated. If my factory were to catch it running inside a unittest, it still would not matter as the import happens outside the unittest.

My question is: is there a way to mock this singleton and replace across the whole application at once?

Replacing the cached instance on the factory is not an option as the references in the classes point to the old object.

It might be a design flaw to put these singleton instances as class variables in the first place. But I'd rather retrieve a class variable than calling the factory each time for the singleton.

2
  • Just a hint regarding singletons in python, which I recently learned and found very interesting: if you need some object being a singleton, you can implement it as a python module and then just import it. It will be instantiated only once no matter the number of imports. Commented Jan 30, 2019 at 8:37
  • Nice to know! Thanks :) Commented Jan 30, 2019 at 8:46

1 Answer 1

1

In your use case, a single module is in charge of providing the singleton to the whole application. So I would try to inject the mock in that module before it is used by anything else. The problem is that the mock cannot be fully constructed before the other classes are declared. A possible way is to construct the singleton in 2 passes: first pass does not depend on anything, then that minimal object is used to construct the classes and then its internal dictionnary is populated. Code could be:

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)

But beware: this assumes that the test procedure does not load model.py or datacontroller.py before test_simple.py

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.