The Artima Developer Community
Sponsored Link

Software Jam Sessions
Stupid datetime testing
by Barry Warsaw
March 7, 2009
Summary
Testing APIs that use the current date and time are a pain because those values are variable. In Mailman 3 I hit upon a really dumb, simple way to do this that doesn't suck.

Advertisement

So here's the problem: Let's say you have a database class that is unit tested. I like doctests, but maybe you like Python unittest. Your class has a column (exposed in your ORM class) for the date that the row was last touched, which is initialized from today's date. How do you test this?

Your test could just ensure that some date was stuffed in the attribute/column, but you can't really tell if it's the date you care about because that's going to be different every day you run the test. So this solution isn't very good.

You could change your API to allow you to pass a known date into it, and your test could call this extended API, but that's not great for several reasons. You'd rather not clutter your API up with extra parameters only used in testing, and it means your ORM class now has different logic for testing environments and production environments. So I don't like this much either.

I'm sure you've thought of your own approaches, including hacking Python's standard datetime module to stuff in instances that allow you to override datetime.datetime.now() and datetime.date.today(). I thought of that too! Because these are built-in types, you can't just replace those methods, but Python does make it fairly easy to subclass built-in types. So that would seem like a good approach except...

I use the Storm ORM in Mailman 3 and it has very strict type checking on its column input values. Generally this is a good thing, but in this case it prevents the subclassing approach because the DateTime column type won't accept subclasses of the built-in datetime type.

The approach I settled on seems the least sucky to me. It's also stupid simple, so I kind of like it for that reason too! I wrote a simple wrapper class that only returns now() and today() and I make sure all the testable call sites call these methods instead of the datetime module's versions. The now() and today() in my utility module then are really instance methods of a class which can be told whether it's in testing mode or not. When it's not in testing mode, it simply returns datetime.datetime.now() and datetime.date.today() as usual. When it is in testing mode, it returns instead a known date and time, which of course, you can test!

The class itself has two additional class methods. One resets the current date and time (in testing mode) to the original known values. The other allows you to fast forward the known date and time. Here's the (stripped down) code. Note that because of the conditional expressions, Python 2.5 is required:

from __future__ import absolute_import, unicode_literals

__metaclass__ = type

import datetime

class DateFactory:
    """A factory for today() and now() that works with testing."""

    # Set to True to produce predictable dates and times.
    testing_mode = False
    # The predictable time.
    predictable_now = None
    predictable_today = None

    def now(self, tz=None):
        return (self.predictable_now
                if self.testing_mode
                else datetime.datetime.now(tz))

    def today(self):
        return (self.predictable_today
                if self.testing_mode
                else datetime.date.today())

    @classmethod
    def reset(cls):
        cls.predictable_now = datetime.datetime(2005, 8, 1, 7, 49, 23)
        cls.predictable_today = cls.predictable_now.date()

    @classmethod
    def fast_forward(cls, days=1):
        cls.predictable_now += datetime.timedelta(days=days)
        cls.predictable_today = cls.predictable_now.date()

factory = DateFactory()
factory.reset()
today = factory.today
now = factory.now

Now, at the call sites, say in your database class, instead of something like:

import datetime
self.date_created = datetime.date.today()

You'd write:

from ... import today
self.date_created = today()

And your test would do something like this:

>>> from ... import factory
>>> factory.testing_mode = True

>>> thing1 = Thing()
>>> thing1.date_created
datetime.datetime(2005, 8, 1, 7, 49, 23)

>>> factory.fast_forward(days=3)

>>> thing2 = Thing()
>>> thing2.date_created
datetime.datetime(2005, 8, 4, 7, 49, 23)

I know there are mocking libraries out there that might help you with this, but I also think this is a nice simple approach where you don't want a lot of extra mocking scaffolding. The cost is that you have to be disciplined not to use the standard datetime module at your call sites.

Talk Back!

Have an opinion? Readers have already posted 4 comments about this weblog entry. Why not add yours?

RSS Feed

If you'd like to be notified whenever Barry Warsaw adds a new entry to his weblog, subscribe to his RSS feed.

About the Blogger

Barry Warsaw has been developing software and playing music for more than 25 years. Since 1995 he has worked in Guido van Rossum's Pythonlabs team. He has been the lead developer of the JPython system, and is now the lead developer of GNU Mailman, a mailing list management system written primarily in Python. He's also a semi-professional musician. Python and the bass are his main axes. Music and software are both at their best when enjoyed, participated in, and shared by their enthusiastic fans and creators.

This weblog entry is Copyright © 2009 Barry Warsaw. All rights reserved.

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use