I am using a home brew datetime.datetime
mock to patch out datetime throughout the code (see at the very bottom), but other people seem to hit problems understanding how it works, and hit unexpected problems. Considered the following test:
@patch("datetime.datetime", FakeDatetime)
def my_test(self):
FakeDatetime.now_value = datetime(2014, 04, 02, 13, 0, 0)
u = User.objects.get(x=y)
u.last_login = datetime(2014, 04, 01, 14, 0, 0)
u.save()
u2 = User.objects.get(x=y)
# Checks if datetime.datetime.now() - u2.last_login < 24 hours
self.assertTrue(u2.logged_in_in_last_24_hours())
Now if you look at how Django DatetimeField serializes dates to SQL:
def to_python(self, value):
if value is None:
return value
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
value = datetime.datetime(value.year, value.month, value.day)
This part gets executed as you call u.save()
in the test.
As this point in the Django code value of value
(u.last_login
) is of type datetime.datetime
because we've assigned the value in the test using an unpatched version of datetime (since our import is
at the module level, and the patch is at the method level).
Now in the Django code, datetime.datetime
is patched, therefore:
isinstance(value, datetime.datetime)
is equivalent to:
isinstance(datetime.datetime(2014, 04, 01, 14, 0, 0), FakeDatetime)
which is False, but:
isinstance(datetime.datetime(2014, 04, 01, 14, 0, 0), datetime.date)
is True hence the datetime.datetime
object gets converted to a
datetime.date
, and when you retrieve u2.last_login
from the SQL, the value is
actually datetime(2014, 04, 01, 0, 0, 0)
and not datetime(2014, 04, 01, 14, 0, 0)
Hence the tests fail.
The way around this is to replace:
u.date_joined = datetime(2014, 04, 01, 14, 0, 0)
with:
u.date_joined = FakeDatetime(2014, 04, 01, 14, 0, 0)
but this seems prone to mistakes and tends to confuse people using or writing the tests.
Especially in cases where you need the real now
value you have to either do datetime_to_fakedatetime(datetime.datetime.now())
or call FakeDatetime.now()
but make sure that the previous test has unset the FakeDatetime.now_value
.
I am looking for a way to make this more intuitive, but at the same time avoid having to patch the datetime.datetime
objects in particular sub modules (as there might be many of them), and just patch it throughout the code.
Code for the homebrew mock:
from datetime import datetime
class FakeDatetime(datetime):
now_value = None
def __init__(self, *args, **kwargs):
return super(FakeDatetime, self).__init__()
@classmethod
def now(cls):
if cls.now_value:
result = cls.now_value
else:
result = datetime.now()
return datetime_to_fakedatetime(result)
@classmethod
def utcnow(cls):
if cls.now_value:
result = cls.now_value
else:
result = datetime.utcnow()
return datetime_to_fakedatetime(result)
# http://stackoverflow.com/questions/20288439/how-to-mock-the-operator-in-python-specifically-datetime-date-datetime-ti
def __add__(self, other):
return datetime_to_fakedatetime(super(FakeDatetime, self).__add__(other))
def __sub__(self, other):
return datetime_to_fakedatetime(super(FakeDatetime, self).__sub__(other))
def __radd__(self, other):
return datetime_to_fakedatetime(super(FakeDatetime, self).__radd__(other))
def __rsub__(self, other):
return datetime_to_fakedatetime(super(FakeDatetime, self).__rsub__(other))
def datetime_to_fakedatetime(dt):
# Because (datetime - datetime) produces a timedelta, so check if the result is of the correct type.
if isinstance(dt, datetime):
return FakeDatetime(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
dt.tzinfo
)
return dt
Thanks!