Django unit testing with date/time-based objects

24,853

Solution 1

EDIT: Since my answer is the accepted answer here I'm updating it to let everyone know a better way has been created in the meantime, the freezegun library: https://pypi.python.org/pypi/freezegun. I use this in all my projects when I want to influence time in tests. Have a look at it.

Original answer:

Replacing internal stuff like this is always dangerous because it can have nasty side effects. So what you indeed want, is to have the monkey patching be as local as possible.

We use Michael Foord's excellent mock library: http://www.voidspace.org.uk/python/mock/ that has a @patch decorator which patches certain functionality, but the monkey patch only lives in the scope of the testing function, and everything is automatically restored after the function runs out of its scope.

The only problem is that the internal datetime module is implemented in C, so by default you won't be able to monkey patch it. We fixed this by making our own simple implementation which can be mocked.

The total solution is something like this (the example is a validator function used within a Django project to validate that a date is in the future). Mind you I took this from a project but took out the non-important stuff, so things may not actually work when copy-pasting this, but you get the idea, I hope :)

First we define our own very simple implementation of datetime.date.today in a file called utils/date.py:

import datetime

def today():
    return datetime.date.today()

Then we create the unittest for this validator in tests.py:

import datetime
import mock
from unittest2 import TestCase

from django.core.exceptions import ValidationError

from .. import validators

class ValidationTests(TestCase):
    @mock.patch('utils.date.today')
    def test_validate_future_date(self, today_mock):
        # Pin python's today to returning the same date
        # always so we can actually keep on unit testing in the future :)
        today_mock.return_value = datetime.date(2010, 1, 1)

        # A future date should work
        validators.validate_future_date(datetime.date(2010, 1, 2))

        # The mocked today's date should fail
        with self.assertRaises(ValidationError) as e:
            validators.validate_future_date(datetime.date(2010, 1, 1))
        self.assertEquals([u'Date should be in the future.'], e.exception.messages)

        # Date in the past should also fail
        with self.assertRaises(ValidationError) as e:
            validators.validate_future_date(datetime.date(2009, 12, 31))
        self.assertEquals([u'Date should be in the future.'], e.exception.messages)

The final implementation looks like this:

from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError

from utils import date

def validate_future_date(value):
    if value <= date.today():
        raise ValidationError(_('Date should be in the future.'))

Hope this helps

Solution 2

You could write your own datetime module replacement class, implementing the methods and classes from datetime that you want to replace. For example:

import datetime as datetime_orig

class DatetimeStub(object):
    """A datetimestub object to replace methods and classes from 
    the datetime module. 

    Usage:
        import sys
        sys.modules['datetime'] = DatetimeStub()
    """
    class datetime(datetime_orig.datetime):

        @classmethod
        def now(cls):
            """Override the datetime.now() method to return a
            datetime one year in the future
            """
            result = datetime_orig.datetime.now()
            return result.replace(year=result.year + 1)

    def __getattr__(self, attr):
        """Get the default implementation for the classes and methods
        from datetime that are not replaced
        """
        return getattr(datetime_orig, attr)

Let's put this in its own module we'll call datetimestub.py

Then, at the start of your test, you can do this:

import sys
import datetimestub

sys.modules['datetime'] = datetimestub.DatetimeStub()

Any subsequent import of the datetime module will then use the datetimestub.DatetimeStub instance, because when a module's name is used as a key in the sys.modules dictionary, the module will not be imported: the object at sys.modules[module_name] will be used instead.

Solution 3

Slight variation to Steef's solution. Rather than replacing datetime globally instead you could just replace the datetime module in just the module you are testing, e.g.:


import models # your module with the Event model
import datetimestub

models.datetime = datetimestub.DatetimeStub()

That way the change is much more localised during your test.

Solution 4

I'd suggest taking a look at testfixtures test_datetime().

Solution 5

What if you mocked the self.end_date instead of the datetime? Then you could still test that the function is doing what you want without all the other crazy workarounds suggested.

This wouldn't let you stub all date/times like your question initially asks, but that might not be completely necessary.

today = datetime.date.today()

event1 = Event()
event1.end_date = today - datetime.timedelta(days=1) # 1 day ago
event2 = Event()
event2.end_date = today + datetime.timedelta(days=1) # 1 day in future

self.assertTrue(event1.is_over())
self.assertFalse(event2.is_over())
Share:
24,853
Fragsworth
Author by

Fragsworth

Developer of Clicker Heroes, Cloudstone, and other games http://www.clickerheroes.com/ http://www.kongregate.com/games/nexoncls/cloudstone http://armorgames.com/cloudstone-game/15364

Updated on April 10, 2020

Comments

  • Fragsworth
    Fragsworth about 4 years

    Suppose I have the following Event model:

    from django.db import models
    import datetime
    
    class Event(models.Model):
        date_start = models.DateField()
        date_end = models.DateField()
    
        def is_over(self):
            return datetime.date.today() > self.date_end
    

    I want to test Event.is_over() by creating an Event that ends in the future (today + 1 or something), and stubbing the date and time so the system thinks we've reached that future date.

    I'd like to be able to stub ALL system time objects as far as python is concerned. This includes datetime.date.today(), datetime.datetime.now(), and any other standard date/time objects.

    What's the standard way to do this?

  • Fragsworth
    Fragsworth about 15 years
    I don't particularly like this solution because it involves complicating production code for the sake of test code, by using nonstandard date/time methods.
  • user1066101
    user1066101 about 15 years
    (a) They're standard datetime.datetime.now() function calls. What's non-standard? (b) All designs should allow for Strategy (or (b) Dependency Injection) because that's how UnitTesting (and architecture) gets done well.
  • Fragsworth
    Fragsworth about 15 years
    This might work for a system that you build from the ground up, but when pulling several 3rd party libraries together (each calling the original datetime.datetime.now()) it can become a maintenance problem. I would like to minimize the amount of 3rd party library code I have to modify, so a solution that changes the results of the original python methods would be ideal.
  • user1066101
    user1066101 about 15 years
    Or, perhaps import mockdatetime as datetime?
  • John Montgomery
    John Montgomery about 15 years
    That would involve changing the code you were testing though wouldn't it? All you really want to do is re-bind the name "datetime" in the models module.
  • John Montgomery
    John Montgomery about 15 years
    At the end of the day it's about leveraging Python's dynamic nature to avoid having to needlessly complicate your code.
  • Carl Meyer
    Carl Meyer about 15 years
    -1 This is Python, not Java. We have first-class functions, therefore any reference to any function is already "the Strategy pattern" because you can reassign that name to point to some other function. Which is exactly what the (better) solutions here do.
  • jamesls
    jamesls about 15 years
    +1 This is much better than replacing sys.modules['datetime']. Swapping sys.modules['datetime'] only works for subsequent imports, and in the sample code in the question, the import of datetime is the second thing that happens. Setting models.datetime allows you to patch the object in the test setUp() and restore it in the tearDown().
  • Aaron
    Aaron almost 14 years
    I'm not sure this will work on a long-running thread like what you'd have in a django + mod_wsgi environment. I think the default today would be compiled the first time your program is loaded and then remain the same until the next time the code was reloaded.
  • slandau
    slandau over 13 years
    I blogged an alternative method to use mock / patch with datetime:voidspace.org.uk/python/weblog/arch_d7_2010_10_02.s‌​html#e1188
  • Danny Staple
    Danny Staple about 13 years
    you could reduce this further: def is_over(self, today=datetime.datetime.now(): return today > self.date_end
  • Eray Erdin
    Eray Erdin about 6 years
    Upon edit: freezegun module might have some issues. I need to do tests based on my timezone. However, freezegun's freeze_time method only lets your give tz_offset argument and Django will look for tzinfo attribute in datetime objects. This has failed some of my test cases. Just bear this in mind while using it.
  • Remco Wendt
    Remco Wendt about 6 years
    @ErdinEray please discuss issues your are having with freezegun with the project's maintainers
  • Eray Erdin
    Eray Erdin about 6 years
    Already done so. Commented just to inform some Googlers. Here is the issue: github.com/spulec/freezegun/issues/246