Mocking default=timezone.now for unit tests

dgel picture dgel · Sep 20, 2013 · Viewed 9.2k times · Source

I'm trying to write unit tests for a django app that does a lot of datetime operations. I have installed mock to monkey patch django's timezone.now for my tests.

While I am able to successfully mock timezone.now when it is called normally (actually calling timezone.now() in my code, I am not able to mock it for models that are created with a DateTimeField with default=timezone.now.


I have a User model that contains the following:

from django.utils import timezone
...
timestamp = models.DateTimeField(default=timezone.now)
modified = models.DateTimeField(default=timezone.now)
...
def save(self, *args, **kwargs):
    if kwargs.pop('modified', True):
        self.modified = timezone.now()
    super(User, self).save(*args, **kwargs)

My unit test looks like this:

from django.utils import timezone

def test_created(self):
    dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
    with patch.object(timezone, 'now', return_value=dt):
        user = User.objects.create(username='test')
        self.assertEquals(user.modified, dt)
        self.assertEquals(user.timestamp, dt)

assertEquals(user.modified, dt) passes, but assertEquals(user.timestamp, dt) does not.

How can I mock timezone.now so that even default=timezone.now in my models will create the mock time?


Edit

I know that I could just change my unit test to pass a timestamp of my choice (probably generated by the mocked timezone.now)... Curious if there is a way that avoids that though.

Answer

cjerdonek picture cjerdonek · Feb 10, 2014

Here's a method you can use that doesn't require altering your non-test code. Just patch the default attributes of the fields you want to affect. For example--

field = User._meta.get_field('timestamp')
mock_now = lambda: datetime(2010, 1, 1)
with patch.object(field, 'default', new=mock_now):
    # Your code here

You can write helper functions to make this less verbose. For example, the following code--

@contextmanager
def patch_field(cls, field_name, dt):
    field = cls._meta.get_field(field_name)
    mock_now = lambda: dt
    with patch.object(field, 'default', new=mock_now):
        yield

would let you write--

with patch_field(User, 'timestamp', dt):
    # Your code here

Similarly, you can write helper context managers to patch multiple fields at once.