如何在Python中更改所有模块的日期/时间?

2024-05-18 11:05:23 发布

您现在位置:Python中文网/ 问答频道 /正文

当我使用业务逻辑编写代码时,我的代码通常取决于当前时间。例如,查看每个未完成订单并检查是否应发送发票(这取决于作业结束后的天数)的算法。在这些情况下,创建发票不是由显式用户操作触发的,而是由后台作业触发的。

现在这给我带来了一个测试问题:

  • 我可以轻松地测试发票创建本身
  • 但是,很难在测试中创建订单并检查后台作业是否在正确的时间标识了正确的订单。

到目前为止,我找到了两个解决方案:

  • 在测试设置中,计算相对于当前日期的作业日期。缺点:代码变得相当复杂,因为不再有明确的日期。有时,对于边缘情况,业务逻辑相当复杂,因此由于所有这些相对日期,调试变得困难。
  • 我有自己的日期/时间访问器函数,在我的代码中使用。在测试中,我只是设置了一个当前日期,所有模块都会得到这个日期。因此,我可以模拟2月份的订单创建,并检查发票是否轻松地在4月份创建。缺点:第三方模块不使用这种机制,因此很难集成和测试这些模块。

第二种方法对我来说毕竟要成功得多。因此,我正在寻找一种方法来设置Python的datetime+time模块返回的时间。设置日期通常就足够了,我不需要设置当前的小时或秒(尽管这会很好)。

有这样的公用事业吗?我可以使用(内部)Python API吗?


Tags: 模块方法代码订单算法作业时间情况
3条回答

freezegun包是专门为此目的而制作的。它允许您更改测试代码的日期。它可以直接使用,也可以通过decorator或上下文管理器使用。一个例子:

from freezegun import freeze_time
import datetime

@freeze_time("2012-01-14")
def test():
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)

有关更多示例,请参见项目:https://github.com/spulec/freezegun

您可以通过创建一个作为代理的自定义日期时间模块(甚至是一个假的——见下面的示例),然后将其插入sys.modules字典来修补系统。从那时起,每次导入datetime模块都将返回您的代理。
仍然有datetime类的警告,特别是当有人这样做时;为此,您只需为该类添加另一个代理即可。

下面是我所说的一个例子——当然,这只是我在5分钟内抛出的一些东西,可能有几个问题(例如,datetime类的类型不正确);但希望它已经有用。

import sys
import datetime as datetime_orig

class DummyDateTimeModule(sys.__class__):
  """ Dummy class, for faking datetime module """
  def __init__(self):
    sys.modules["datetime"] = self
  def __getattr__(self, attr):
    if attr=="datetime":
      return DummyDateTimeClass()
    else:
      return getattr(datetime_orig, attr)

class DummyDateTimeClass(object):
  def __getattr__(self, attr):
    return getattr(datetime_orig.datetime, attr)

dt_fake = DummyDateTimeModule()

最后-值得吗?
坦率地说,我更喜欢我们的第二个解决方案:)。
是的,python是一种非常动态的语言,在这里您可以做很多有趣的事情,但是以这种方式修补代码总是有一定的风险,即使我们在这里讨论的是测试代码。
但是,我认为附件函数会使测试补丁更加明确,而且您的代码在测试内容方面也会更加明确,从而提高可读性。
因此,如果改变不是太贵的话,我会采用你的第二种方法。

实际上,Monkey补丁time.time可能已经足够了,因为它为Python中几乎所有其他基于时间的例程提供了基础。这似乎可以很好地处理您的用例,而无需使用更复杂的技巧,而且何时执行也无所谓(除了少数stdlib包,如Queue.py和threading.py,在这种情况下,您必须在导入它们之前进行修补):

>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2010, 4, 17, 14, 5, 35, 642000)
>>> import time
>>> def mytime(): return 120000000.0
...
>>> time.time = mytime
>>> datetime.datetime.now()
datetime.datetime(1973, 10, 20, 17, 20)

也就是说,在多年来为各种类型的自动化测试模拟对象的过程中,我很少需要这种方法,因为大多数时候需要模拟的是我自己的应用程序代码,而不是stdlib例程。毕竟,你知道他们已经开始工作了。如果遇到自己的代码必须处理库例程返回的值的情况,则可能需要模拟库例程本身,至少在检查自己的应用程序如何处理时间戳时是这样。

到目前为止,最好的方法是构建您自己的日期/时间服务例程,您只在应用程序代码中使用这些例程,并在其中构建测试功能,以便根据需要提供假结果。例如,有时我会做一个更复杂的类似的事情:

# in file apptime.py (for example)
import time as _time

class MyTimeService(object):
    def __init__(self, get_time=None):
        self.get_time = get_time or _time.time

    def __call__(self):
        return self.get_time()

time = MyTimeService()

现在在我的应用程序代码中,我只需要import apptime as time; time.time()来获取当前时间值,而在测试代码中,我可以首先在apptime.time = MyTimeService(mock_time_func)代码中setUp()来提供假时间结果。

更新:几年后有一个替代方案,如Dave Forgac's answer所述。

相关问题 更多 >