【问题标题】:Mocking default=timezone.now for unit tests模拟单元测试的 default=timezone.now
【发布时间】:2013-09-26 07:40:40
【问题描述】:

我正在尝试为执行大量日期时间操作的 django 应用程序编写单元测试。我已经为我的测试安装了mock 到猴子补丁 django 的timezone.now

虽然我能够在正常调用timezone.now 时成功模拟它(实际上在我的代码中调用timezone.now(),但我无法模拟使用DateTimeFielddefault=timezone.now 创建的模型。


我有一个 User 模型,其中包含以下内容:

from django.utils import timezone
...
timestamp = models.DateTimeField(default=timezone.now)
modified = models.DateTimeField(default=timezone.now)
...
def save(self, *args, **kwargs):
    if kwargs.pop('modified', True):
        self.modified = timezone.now()
    super(User, self).save(*args, **kwargs)

我的单元测试如下所示:

from django.utils import timezone

def test_created(self):
    dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
    with patch.object(timezone, 'now', return_value=dt):
        user = User.objects.create(username='test')
        self.assertEquals(user.modified, dt)
        self.assertEquals(user.timestamp, dt)

assertEquals(user.modified, dt) 通过,但 assertEquals(user.timestamp, dt) 不通过。

我如何模拟 timezone.now 以便即使在我的模型中的 default=timezone.now 也会创建模拟时间?


编辑

我知道我可以更改我的单元测试以通过我选择的timestamp(可能由模拟的timezone.now 生成)...很好奇是否有办法避免这种情况。

【问题讨论】:

  • 您能否提供User 模型的完整相关列表?了解默认值绑定发生在什么时候非常重要。

标签: python django unit-testing mocking python-mock


【解决方案1】:

还有另一种简单的方法可以完成上述操作。

import myapp.models.timezone
from unittest.mock import patch

@patch('django.utils.timezone.now')
def test_created(self, mock_timezone):
    dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
    mock_timezone.return_value = dt
    user = User.objects.create(username='test')

    self.assertEquals(user.modified, dt)
    self.assertEquals(user.timestamp, dt)

这是模拟 timezone.now 的最佳方式。

【讨论】:

  • 这对我来说似乎不适用于 Django 2.1.5 和 python 3.6.7
  • @chrisbu 请注意,带有auto_add_now 的字段不会被覆盖,因为auto_add_now 是数据库级指令而不是python 指令。 Django 开发人员决定不改变这种奇怪的行为。参考:code.djangoproject.com/ticket/16583
【解决方案2】:

这是一种无需更改非测试代码即可使用的方法。只需修补您想要影响的字段的default 属性。比如——

field = User._meta.get_field('timestamp')
mock_now = lambda: datetime(2010, 1, 1)
with patch.object(field, 'default', new=mock_now):
    # Your code here

您可以编写辅助函数来减少冗长。比如下面的代码--

@contextmanager
def patch_field(cls, field_name, dt):
    field = cls._meta.get_field(field_name)
    mock_now = lambda: dt
    with patch.object(field, 'default', new=mock_now):
        yield

会让你写——

with patch_field(User, 'timestamp', dt):
    # Your code here

同样,您可以编写帮助上下文管理器来一次修补多个字段。

【讨论】:

  • 这种方法的问题是如果default参数没有提供给该字段,测试不会失败,所以它应该失败时通过
  • 我不确定我是否遵循。你有一个例子吗?您是否有任何理由不能在测试中添加额外的断言以确保在您担心的情况下发生故障?
  • 示例是从时区字段的声明中删除default=timezone.now 并检查测试是否通过。我的经验是,由于没有默认值,我预计它会失败时测试通过了。
  • 我们想在没有提供值的情况下测试该字段是否设置为当前日期。这需要一个返回当前日期的可调用函数,以将其传递给default 参数,但是通过修补default 参数,我们实际上会覆盖那里的所有内容,因此我们不会像在生产环境中那样测试代码。通过模拟默认值,我们实际上测试了 django 是否正确使用了默认参数!因此,这使得补丁变得相当多余。最后,我选择检查默认设置为timezone.nowself.assertEqual(event_datetime_field.default, timezone.now)
  • 在这种情况下,是的,您不会想使用这种方法。这种方法的目的是确保在您的测试中创建的测试对象已知(例如硬编码)日期时间值,而不是根据当前实际日期时间而变化的值。因此,它并不是为了测试字段本身,而是作为一种支持技术,以确保使用这些对象的测试是确定性/可重现的。
【解决方案3】:

我自己也遇到了这个问题。问题是模型是在 mock 修补 timezone 模块之前加载的,所以在计算表达式 default=timezone.now 时,它将 default kwarg 设置为真正的 timezone.now 函数。

解决方法如下:

class MyModel(models.Model):
    timestamp = models.DateTimeField(default=lambda: timezone.now())

【讨论】:

  • 嗨,@Jordan,在更改此默认值并运行 python manage.py makemigrations 命令后,我收到如下错误:"raise ValueError("Cannot serialize function: lambda")", my python是3.5.3版,django是1.10.5
  • 不要那样做! django 无法序列化 lambda,它会在 makemigrations 期间出现异常。相反,在 utils 包def now(): return timezone.now() 中创建一个包装函数并在任何地方使用它
【解决方案4】:

看起来您正在修补wrong place. 中的时区

假设您的 User 模型位于 myapp\models.py 中,并且您想在该文件中测试 save()。问题是当你在顶部from django.utils import timezone 时,它会从django.utils 导入它。在您的测试中,您正在本地修补timezone,它对您的测试没有影响,因为模块myapp\models.py 已经引用了真正的timezone,看起来我们的修补没有效果。

尝试从myapp\models.py 修补timezone,类似于:

import myapp.models.timezone

def test_created(self):
    with patch('myapp.models.timezone') as mock_timezone:
        dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
        mock_timezone.now.return_value = dt

        assert myapp.models.timezone.now() == dt

        user = User.objects.create(username='test')
        self.assertEquals(user.modified, dt)
        self.assertEquals(user.timestamp, dt)

【讨论】:

  • 好收获。不幸的是,从我的模型中导入 timezone 似乎没有任何区别。
  • 举个例子试试看:你必须在模块级别上打补丁,而不仅仅是对象。
  • 看起来默认值是在测试(和修补)运行之前设置的。在测试运行之前可能会发生一些严重的 django 魔法。
  • @Oleksiy 神奇之处如下:auto_add_now 是数据库级指令,而不是 python 指令。 Django 开发人员决定不改变这种奇怪的行为。参考:code.djangoproject.com/ticket/16583
猜你喜欢
  • 2013-02-22
  • 2011-04-11
  • 2020-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
相关资源
最近更新 更多