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:
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.
I've used very similar concepts in the past, but inject a clock instead of a DateFactory (using different "clocks" to achieve different results). I'd written something quite recent about how to implement this in java (link below).
I would recommend separating the responsibility for fetching current time/date/days from being able to manipulate it. I tend to think the manipulation is really a testing aspect only.