如何在Python中更改所有模块的日期时间?
当我处理业务逻辑时,我的代码常常需要依赖当前的时间。例如,有一个算法会查看每个未完成的订单,并检查是否应该发送发票(这取决于工作结束后的天数)。在这种情况下,创建发票并不是由用户的明确操作触发的,而是由后台任务自动完成的。
这就给我在测试时带来了问题:
- 我可以很容易地测试发票的创建
- 但是,在测试中创建一个订单并检查后台任务是否在正确的时间识别出正确的订单就比较困难。
到目前为止,我找到了解决这个问题的两种方法:
- 在测试设置中,根据当前日期计算任务的日期。缺点是:代码变得相当复杂,因为不再有明确的日期。有时候,业务逻辑在一些边缘情况下相当复杂,因此由于这些相对日期,调试起来会很困难。
- 我有自己的日期/时间访问函数,整个代码中都在使用。在测试中,我只需设置一个当前日期,所有模块都会使用这个日期。这样我就可以轻松模拟在二月份创建订单,并检查发票是否在四月份生成。缺点是:第三方模块不使用这个机制,所以很难进行集成和测试。
经过一番尝试,第二种方法对我来说效果更好。因此,我在寻找一种方法来设置Python的datetime和time模块返回的时间。设置日期通常就足够了,我不需要设置当前的小时或秒(尽管这样做会更好)。
有没有这样的工具?有没有可以使用的(内部)Python API?
6 个回答
你可以通过创建一个自定义的日期时间模块(甚至可以是一个假的模块,下面有个例子)来修补系统,这个模块就像一个代理,然后把它放进 sys.modules 字典里。从那以后,每次导入日期时间模块时,都会返回你的代理模块。
不过有个小问题,就是关于 datetime 类,特别是当有人使用 from datetime import datetime
时;对此,你可以单独为这个类添加另一个代理。
下面是我说的例子——当然,这只是我花了五分钟随便写的,可能会有一些问题(比如,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-patching)time.time
其实就够用了,因为它是Python中几乎所有与时间相关的功能的基础。这种方法能很好地满足你的需求,不需要使用更复杂的技巧,而且你什么时候去做都没关系(除了少数几个标准库,比如Queue.py和threading.py,它们会用到from time import time
,在这种情况下你必须在它们被导入之前就进行补丁处理):
>>> 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)
不过,经过多年的对象模拟(mocking)用于各种自动化测试,我发现这种方法其实很少用到,因为大多数情况下需要模拟的是我自己写的应用代码,而不是标准库的功能。毕竟,标准库的功能你知道是好用的。如果你遇到需要处理库函数返回值的情况,可能需要模拟这些库函数,至少在检查你的应用如何处理时间戳时是这样。
最好的方法是自己建立一个日期/时间服务例程,只在你的应用代码中使用,并且在这个例程中加入测试时可以提供假结果的能力。例如,我有时会做一个更复杂的类似操作:
# 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()
来获取当前时间,而在测试代码中,我可以先在setUp()
代码中执行apptime.time = MyTimeService(mock_time_func)
来提供假时间结果。
更新:多年后,有了另一种选择,如Dave Forgac的回答中所提到的。
freezegun这个包是专门为这个目的而制作的。它可以让你在测试代码的时候改变日期。你可以直接使用它,也可以通过装饰器或者上下文管理器来使用。举个例子:
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