4

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)

Source

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!

4

1 回答 1

3

https://github.com/spulec/freezegun可以与 Django 一起使用。

于 2014-04-01T16:01:19.717 回答