Django Testing: Dealing with datetimes

December 26, 2015

While writing automated tests for Django you soon or later will need to control the internal clock of the system. There are several obvious ways to do that and I will try to point out the pros and cons of each one.

Although I’m saying Django most of those ideas can be applied to any python application.

Receive the datetime object as an optional argument

This is the simplest way. The idea here is to pass a function as argument that returns the current datetime, e.g.

The is_expired implementation must get the datetime calling the function now. Defaults to django.utils.timezone#now.

class Promotion(models.Model):

  def is_expired(self, now=timezone.now):
    system_datetime = now()
    ...
def test_is_expired(self):
  now = lambda: datetime(2020, 10, 10)
  assert self.promotion.is_expired(now=now) == True

Pros

  • Simplicity.

Cons

  • Dirty and confusing interfaces.

Mock the standard datetime library

Using the mock library to do that, e.g.

@patch('yourpackage.datetime')
def test_is_expired(self, datetime_mock):
  datetime_mock.now.return_value = datetime(2020, 10, 10)
  assert self.promotion.is_expired() == True

Pros

  • Fits very well with the python development philosophy;
  • Moderate complexity.

Cons

  • Troublesome if the datetime needs to be mocked system-wide, e.g. in the development server.

Service Interface

We can call this the most object oriented solution to the problem. Create a service interface that provides a method to retrieve the time, and then mock it as needed. e.g.

class TimeService(object):
  
  def now(self):
    return timezone.now()

The is_expired implementation must get the datetime through TimeService#now.

class PromotionService(object):

  def __init__(self, timeservice):
    self._timeservice = timeservice

  def is_expired(promotion):
    system_datetime = self._timeservice.now()
    ...

The example is using the create_autospec function from python standard library.

def test_is_expired(self):
  timeservice = create_autospec(TimeService)
  timeservice.now.return_value = datetime(2020, 10, 10)

  service = PromotionService(timeservice)

  assert service.is_expired(self.promotion) == True

Pros

  • High cohesion, low coupling;
  • Clear interfaces, dependencies are explicitly set in the constructor;
  • Easy to replace the TimeService with a fake one and apply it system-wide.

Cons

  • Complex and verbose;
  • Doesn’t fits well with the python development philosophy.

Conclusion

Neither of the solutions is perfect and thus people generally choose between them based on their background and programming style. Do you know another solution? Share with us in the comments!

comments powered by Disqus