django 测试中的模拟时间问题:使用 freezegun 似乎没有冻结时间

Posted

技术标签:

【中文标题】django 测试中的模拟时间问题:使用 freezegun 似乎没有冻结时间【英文标题】:Mocking time issue in django test: time seems not to be frozen using freezegun 【发布时间】:2017-06-18 06:52:07 【问题描述】:

我在这里编写一个功能测试来检查我的 API 限制是否按预期工作(将在每个月初休息)。

测试类:

class ApiThrottlingTest(ThrottlingBaseTest):

    def test_throttling_purchaser_case(self):

        now = datetime.datetime(year=2015, month=1, day=10, hour=6, minute=6, second=3)

        last_day_of_current_month = datetime.datetime(year=2015, month=1, day=31, hour=23, minute=59, second=59)

        first_day_of_next_month = datetime.datetime(year=2015, month=2, day=1, hour=0, minute=0, second=0)

        with freeze_time(now) as frozen_datetime:
            for i in xrange(3):
                resp = self._project_details_request()
                self.assertEqual(resp.status_code, 200)

            resp = self._project_details_request()
            self.assertEqual(resp.status_code, 429)

            frozen_datetime.move_to(last_day_of_current_month)
            resp = self._project_details_request()
            # the test fails at this level
            self.assertEqual(resp.status_code, 429)

            frozen_datetime.move_to(first_day_of_next_month)
            resp = self._project_details_request()
            self.assertEqual(resp.status_code, 200)

如果:last_day_of_current_month = datetime.datetime(... second=0),测试工作正常 但会在以下情况下失败:last_day_of_current_month = datetime.datetime(... second=59)

在调试之后,DjangoRestFramework throttling.UserRateThrottle 中使用的 time 模块似乎在某种程度上给出了一个始终领先于我的测试中的冻结时间的值,这导致了几秒钟的精度问题。

基于FreezeGun Doc time.time() 也应该被冻结:

一旦调用了装饰器或上下文管理器,所有对 datetime.datetime.now()、datetime.datetime.utcnow()、datetime.date.today()、time.time()、time.localtime() 的调用)、time.gmtime() 和 time.strftime() 将返回已冻结的时间。

但看起来我的情况 time.time 正确地采用了模拟日期时间的开始时间,但随着时间的推移不断变化,这是意料之外的,预计它会被冻结,直到手动转发时间。

我尝试使用mock 模块单独模拟UserRateThrottle 中使用的time.time,但仍然没有解决问题。

---->知道可能是什么问题,如何解决?

测试失败:(将时间转发到该月的最后一天:第 14 行

self.assertEqual(resp.status_code, 429) 断言错误:200 != 429

DRF 类源代码

class SimpleRateThrottle(BaseThrottle):
    ...

    cache = default_cache
    timer = time.time 
    cache_format = 'throttle_%(scope)s_%(ident)s'

    def __init__(self):
       ....

    def allow_request(self, request, view):
        ...

        self.now = self.timer() # here timer() returns unexpected value in test
        ....

【问题讨论】:

【参考方案1】:

如果你使用 pytest,你可以这样做:

import pytest
from time import time
from rest_framework.throttling import SimpleRateThrottle


@pytest.mark.freeze_time('2020-09-23 00:00:00')
def test_something(monkeypatch):
    monkeypatch.setattr(SimpleRateThrottle, 'timer', lambda _: time())

    # … unit test here …

【讨论】:

【参考方案2】:

您需要用 FreezeGun 的 time.time 覆盖 SimpleRateThrottle 的计时器。

这里发生的情况是,feezegun 可能会覆盖 Python 的时间模块。但是SimpleRateThrottle并没有使用模块,而是使用了冻枪无法触及的模块功能。

SimpleRateThrottle 因此使用 Python 标准库时间模块,而其他部分代码使用 freezegun 的。

编辑: 你应该这样做 - 在 FreezeGun 被激活后:

former_timer = SimpleRateThrottle.timer
SimpleRateThrottle.timer = time.time

一旦你的测试结束(在 tearDown 或其他类似的东西中):

SimpleRateThrottle.timer = former_timer

请注意,您也可以使用猴子补丁库来为您处理。

【讨论】:

我怎样才能用 FreezeGun 覆盖它!?实际上我考虑了一下并尝试使用python mock模块手动使用猴子补丁来模拟SimpleRateThrottle使用的module'function的返回值,但仍然有同样的问题!!! 对不起,在我写完答案后才意识到。它现在已经编辑了一个操作方法。 谢谢,但还是有同样的问题。 可能是你/drf 在测试之前实例化了SimpleRateThrottle

以上是关于django 测试中的模拟时间问题:使用 freezegun 似乎没有冻结时间的主要内容,如果未能解决你的问题,请参考以下文章

使用无脂肪框架进行单元测试

使用 malloc/free 模拟 new/delete

在 Django 的基于类的视图中模拟函数

6.Django基于模拟浏览器的测试用例编写python-slugifydjango-taggit

在 Django 2 中模拟 RelatedManager

在 django 中测试自定义管理操作