Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python - Make unit tests independent for classes with class variables

I have a class with a dictionary that is used to cache response from server for a particular input. Since this is used for caching purpose, this is kept as a class variable.

class MyClass:
    cache_dict = {}

    def get_info_server(self, arg):
        if arg not in self.cache_dict:
            self.cache_dict[arg] = Client.get_from_server(arg)
        return cache_dict[arg]

    def do_something(self, arg):
        # Do something based on get_info_server(arg)

And when writing unit tests, since the dictionary is a class variable, the values are cached across test cases.

Test Cases

# Assume that Client is mocked.

def test_caching():
    m = MyClass()
    m.get_info_server('foo')
    m.get_info_server('foo')
    mock_client.get_from_server.assert_called_with_once('foo')

def test_do_something():
    m = MyClass()
    mock_client.get_from_server.return_value = 'bar'
    m.do_something('foo') # This internally calls get_info_server('foo')

If test_caching executes first, the cached value will be some mock object. If test_do_something executes first, then the assertion that the test case is called exactly once will fail.

How do I make the tests independent of each other, besides manipulating the dictionary directly (since this is like requiring intimate knowledge of the inner working of the code. what if the inner working were to change later. All I need to verify is the API itself, and not rely on the inner workings)?

like image 250
Kenpachi Avatar asked Oct 28 '22 21:10

Kenpachi


1 Answers

You can't really avoid resetting your cache here. If you are unittesting this class, then your unittest will need to have an intimate knowledge of the inner workings of the class, so just reset the cache. You rarely can change how your class works without adjusting your unittests anyway.

If you feel that that still will create a maintenance burden, then make cache handling explicit by adding a class method:

class MyClass:
    cache_dict = {}

    @classmethod
    def _clear_cache(cls):
        # for testing only, hook to clear the class-level cache.
        cls.cache_dict.clear()

Note that I still gave it a name with a leading underscore; this is not a method that a 3rd party should call, it is only there for tests. But now you have centralised clearing the cache, giving you control over how it is implemented.

If you are using the unittest framework to run your tests, clear the cache before each test in a TestCase.setUp() method. If you are using a different testing framework, that framework will have a similar hook. Clearing the cache before each test ensures that you always have a clean state.

Do take into account that your cache is not thread safe, if you are running tests in parallel with threading you'll have issues here. Since this also applies to the cache implementation itself, this is probably not something you are worried about right now.

like image 90
Martijn Pieters Avatar answered Nov 15 '22 05:11

Martijn Pieters