在Django单元测试中修补日期时间的最佳方法

时间:2014-04-01 13:35:44

标签: python django datetime testing python-mock

我正在使用家酿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)

Source

当您在测试中调用u.save()时,此部分将被执行。 因为Django代码值valueu.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)

为True,因此datetime.datetime对象转换为a datetime.date,当您从SQL中检索u2.last_login时,值为 实际上是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://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

谢谢!

1 个答案:

答案 0 :(得分:3)

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