djsumdog
djsumdog

Reputation: 2710

Running unit tests in Python with a caching decorator

So I'm working on an application that, upon import of certain records, requires some fields to be recalculated. To prevent a database read for each check, there is a caching decorator so the database read is only preformed once every n seconds during import. The trouble comes with building test cases. The following does work, but it has an ugly sleep in it.

# The decorator I need to patch

@cache_function_call(2.0)
def _latest_term_modified():
    return PrimaryTerm.objects.latest('object_modified').object_modified


# The 2.0 sets the TTL of the decorator. So I need to switch out 
# self.ttl for this decorated function before 
# this test. Right now I'm just using a sleep, which works

    @mock.patch.object(models.Student, 'update_anniversary')
    def test_import_on_term_update(self, mock_update):
        self._import_student()
        latest_term = self._latest_term_mod()
        latest_term.save()
        time.sleep(3)
        self._import_student()
        self.assertEqual(mock_update.call_count, 2)

The decorator itself looks like the following:

class cache_function_call(object):
    """Cache an argument-less function call for 'ttl' seconds."""
    def __init__(self, ttl):
        self.cached_result = None
        self.timestamp = 0
        self.ttl = ttl

    def __call__(self, func):
        @wraps(func)
        def inner():
            now = time.time()
            if now > self.timestamp + self.ttl:
                self.cached_result = func()
                self.timestamp = now
            return self.cached_result
        return inner

I have attempted to set the decorator before the import of the models:

decorators.cache_function_call = lambda x : x
import models

But even at the top of the file, django still initializes the models before running my tests.py and the function still gets decorated with the caching decorator instead of my lambda/noop one.

What's the best way to go about writing this test so I don't have a sleep. Can I set the ttl of the decorator before running my import somehow?

Upvotes: 1

Views: 1101

Answers (1)

Alex Martelli
Alex Martelli

Reputation: 882721

You can change the decorator class just a little bit.

At module level in decorators.py set the global

BAILOUT = False

and in your decorator class, change:

def __call__(self, func):
    @wraps(func)
    def inner():
        now = time.time()
        if BAILOUT or now > self.timestamp + self.ttl:
            self.cached_result = func()
            self.timestamp = now
        return self.cached_result
    return inner

Then in your tests set decorators.BAILOUT = True, and, hey presto!-)

Upvotes: 1

Related Questions