Red Hot Chili Python

Python double-call hack for mocking date/time

«  Bad parts of django   ::   Contents   ::   (Git/Mercurial) Clone per feature workflow  »

Python double-call hack for mocking date/time

In unit- and functional-testing you want to somehow deal with dates, and when you run test you want to set your own date (result of utcnow()) call.

With Mock module you have great patch method to patch things (or p method from my mockstar enhances on Mock library). How it works is basically it goes to module you tell it to and replaces __dict__["member"] to your var. Consider an example:

# proj/foo/bar.py

import datetime

def my_great_function():
    current_time = datetime.datetime.utcnow()
    if current_time.year > 2012:
        return 'new'
    else:
        return 'old'

To test this function with your own time, what you can do with patch is:

# tests/proj/foo/bar_test.py

from mock import patch
from proj.foo.bar import my_great_function
from proj.core.test import BaseTestCase

class TestMyGreatFunction(BaseTestCase):
    @patch('proj.foo.bar.datetime')
    def test_should_get_new_for_year_2013(self, datetime_mock):
        datetime_mock.datetime.utcnow.return_value = (
            datetime.datetime(2013, 01, 01))

        result = my_great_function()

        self.assertEquals(result, 'new')

    @patch('proj.foo.bar.datetime')
    def test_should_get_old_for_year_2012(self, datetime_mock):
        datetime_mock.datetime.utcnow.return_value = (
            datetime.datetime(2012, 01, 01))

        result = my_great_function()

        self.assertEquals(result, 'old')

That’s cool, but there are some problems with this code:

  1. Most of the time you don’t want to replace the whole datetime module to mock since you want to have datetime.datetime instances to keep being real.

  2. On functional tests, you want to patch all the utcnow() occurrences (in your code at least), and you don’t want to patch all the modules that use utcnow.

  3. Consider this example:

    # proj/foo/models.py
    
    class MyModel(models.Model):
        my_field = models.DateTimeField(default=utcnow)
    

    If you try to test it by mocking proj.foo.models.utcnow – you will fail. The reason for this is that, when you do usual function call (utcnow()) in your code – python searches for utcnow symbol in __dict__ of your module, and then returns instance for that (which can be already patched), while with this example, you “lock-out” link to your utcnow function object at import-time, and python won’t look for it in __dict__ anymore.

So, what’s the end-solution?

The solution (hack?) is to have something like this:

# proj/util/date.py

import datetime

def _utcnow():
    return datetime.datetime.utcnow()

def utcnow():
    return _utcnow()

You should use proj.util.date.utcnow function everywhere in your project where you want to get current time, and patching it would look something like this:

class TestCurrentTime(FunctionalTestCase):
    @patch("proj.util.date._utcnow")
    def test_should_get_current_time(self, utcnow_mock):
        utcnow_mock.return_value = datetime.datetime(2011, 01, 02)
        result = self.client.get("/api/time/current")
        self.assertEquals(result.content,
                          "2011-01-02")

Basically, every time proj.util.date.utcnow is called, it calls proj.util.date._utnow, which makes python to look into proj.util.date.__dict__ for it, and it find your patched version!

«  Bad parts of django   ::   Contents   ::   (Git/Mercurial) Clone per feature workflow  »

blog comments powered by Disqus