Лучший способ исправления даты и времени в модульных тестах Django

Я использую домашний напиток datetime.datetime макет для исправления даты и времени по всему коду (см. в самом низу), но другие люди, похоже, сталкиваются с проблемами, понимая, как это работает, и сталкиваются с неожиданными проблемами. Рассматривается следующий тест:

@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())

Теперь, если вы посмотрите, как Django DatetimeField сериализует даты в 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)

Источник

Эта часть исполняется, как вы звоните u.save() в тесте. Как этот момент в значении кода Django value (u.last_login) имеет тип datetime.datetime потому что мы присвоили значение в тесте, используя непатченную версию datetime (поскольку наш импорт находится на уровне модуля, а исправление - на уровне метода).

Теперь в коде Django, datetime.datetime исправлен, поэтому:

isinstance(value, datetime.datetime)

эквивалентно:

isinstance(datetime.datetime(2014, 04, 01, 14, 0, 0), FakeDatetime)

что неверно, но:

isinstance(datetime.datetime(2014, 04, 01, 14, 0, 0), datetime.date)

верно, следовательно, datetime.datetime объект превращается вdatetime.dateи когда вы получите u2.last_login из SQL, значение на самом деле datetime(2014, 04, 01, 0, 0, 0) и не datetime(2014, 04, 01, 14, 0, 0)

Следовательно, тесты не пройдены.

Способ обойти это заменить:

u.date_joined = datetime(2014, 04, 01, 14, 0, 0)

с:

u.date_joined = FakeDatetime(2014, 04, 01, 14, 0, 0)

но это кажется склонным к ошибкам и приводит в замешательство людей, использующих или пишущих тесты.

Особенно в тех случаях, когда вам нужен настоящий now значение, которое вы должны сделать datetime_to_fakedatetime(datetime.datetime.now()) или позвоните по телефону FakeDatetime.now() но убедитесь, что предыдущий тест FakeDatetime.now_value,

Я ищу способ сделать это более интуитивным, но в то же время избегать необходимости исправления datetime.datetime объекты в определенных подмодулях (их может быть много), и просто исправьте их по всему коду.

Код для доморощенного макета:

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://stackru.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

Спасибо!

1 ответ

Решение

Существует https://github.com/spulec/freezegun который работает с Django.

Другие вопросы по тегам