使用基于日期/时间的对象进行 Django 单元测试
假设我有以下 Event
模型:
from django.db import models
import datetime
class Event(models.Model):
date_start = models.DateField()
date_end = models.DateField()
def is_over(self):
return datetime.date.today() > self.date_end
我想通过创建一个在未来结束的 Event(今天 + 1 或其他)来测试 Event.is_over()
,并存根日期和时间,以便系统认为我们已经到达了未来的日期。
就 python 而言,我希望能够存根所有系统时间对象。 这包括 datetime.date.today()
、datetime.datetime.now()
以及任何其他标准日期/时间对象。
执行此操作的标准方法是什么?
Suppose I have the following Event
model:
from django.db import models
import datetime
class Event(models.Model):
date_start = models.DateField()
date_end = models.DateField()
def is_over(self):
return datetime.date.today() > self.date_end
I want to test Event.is_over()
by creating an Event that ends in the future (today + 1 or something), and stubbing the date and time so the system thinks we've reached that future date.
I'd like to be able to stub ALL system time objects as far as python is concerned. This includes datetime.date.today()
, datetime.datetime.now()
, and any other standard date/time objects.
What's the standard way to do this?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
编辑:由于我的答案是这里接受的答案,我正在更新它,让每个人都知道同时创建了更好的方法, freezegun 库: https://pypi.python.org/pypi/freezegun。 当我想影响测试时间时,我在所有项目中都会使用它。 看看它。
原始答案:
替换这样的内部东西总是很危险的,因为它可能会产生令人讨厌的副作用。 所以你真正想要的是让猴子补丁尽可能本地化。
我们使用 Michael Foord 的优秀模拟库: http://www.voidspace.org.uk/python/ mock/ 具有一个
@patch
装饰器,可以修补某些功能,但猴子补丁仅存在于测试函数的范围内,并且在函数运行完后,所有内容都会自动恢复范围。唯一的问题是内部
datetime
模块是用 C 实现的,因此默认情况下您无法对其进行猴子修补。 我们通过制作自己的简单实现来解决这个问题,可以被模拟。总的解决方案是这样的(示例是 Django 项目中使用的验证器函数,用于验证日期是否是未来的)。 请注意,我从一个项目中获取了这个,但取出了不重要的东西,所以在复制粘贴这个时,事情可能实际上不起作用,但我希望你明白了:)
首先,我们定义我们自己的非常简单的
datetime.date.today
在一个名为utils/date.py
的文件中:然后我们在
tests.py
中为此验证器创建单元测试:最终实现看起来像这样:
希望这有帮助
EDIT: Since my answer is the accepted answer here I'm updating it to let everyone know a better way has been created in the meantime, the freezegun library: https://pypi.python.org/pypi/freezegun. I use this in all my projects when I want to influence time in tests. Have a look at it.
Original answer:
Replacing internal stuff like this is always dangerous because it can have nasty side effects. So what you indeed want, is to have the monkey patching be as local as possible.
We use Michael Foord's excellent mock library: http://www.voidspace.org.uk/python/mock/ that has a
@patch
decorator which patches certain functionality, but the monkey patch only lives in the scope of the testing function, and everything is automatically restored after the function runs out of its scope.The only problem is that the internal
datetime
module is implemented in C, so by default you won't be able to monkey patch it. We fixed this by making our own simple implementation which can be mocked.The total solution is something like this (the example is a validator function used within a Django project to validate that a date is in the future). Mind you I took this from a project but took out the non-important stuff, so things may not actually work when copy-pasting this, but you get the idea, I hope :)
First we define our own very simple implementation of
datetime.date.today
in a file calledutils/date.py
:Then we create the unittest for this validator in
tests.py
:The final implementation looks like this:
Hope this helps
您可以编写自己的日期时间模块替换类,实现要替换的日期时间中的方法和类。 例如:
让我们将其放入其自己的模块中,我们将调用
datetimestub.py
然后,在测试开始时,您可以执行以下操作:
datetime
的任何后续导入> 然后模块将使用datetimestub.DatetimeStub
实例,因为当模块的名称用作sys.modules
字典中的键时,该模块将不会被导入:将使用sys.modules[module_name]
中的对象。You could write your own datetime module replacement class, implementing the methods and classes from datetime that you want to replace. For example:
Let's put this in its own module we'll call
datetimestub.py
Then, at the start of your test, you can do this:
Any subsequent import of the
datetime
module will then use thedatetimestub.DatetimeStub
instance, because when a module's name is used as a key in thesys.modules
dictionary, the module will not be imported: the object atsys.modules[module_name]
will be used instead.Steef 的解决方案略有不同。 您可以只替换正在测试的模块中的日期时间模块,而不是全局替换日期时间,例如:
这样,在测试期间更改就更加本地化。
Slight variation to Steef's solution. Rather than replacing datetime globally instead you could just replace the datetime module in just the module you are testing, e.g.:
That way the change is much more localised during your test.
我建议看看 testfixtures test_datetime()。
I'd suggest taking a look at testfixtures test_datetime().
如果您嘲笑 self.end_date 而不是日期时间怎么办? 然后,您仍然可以测试该函数是否正在执行您想要的操作,而无需建议所有其他疯狂的解决方法。
这不会让您像您的问题最初询问的那样存根所有日期/时间,但这可能不是完全必要的。
What if you mocked the self.end_date instead of the datetime? Then you could still test that the function is doing what you want without all the other crazy workarounds suggested.
This wouldn't let you stub all date/times like your question initially asks, but that might not be completely necessary.
这不会执行系统范围的日期时间替换,但如果您厌倦了尝试让某些东西正常工作,您可以随时添加一个可选参数以使其更容易测试。
This doesn't perform system-wide datetime replacement, but if you get fed up with trying to get something to work you could always add an optional parameter to make it easier for testing.
两个选择。
通过提供您自己的日期时间来模拟日期时间。 由于在标准库目录之前搜索本地目录,因此您可以将测试放在具有您自己的模拟版本日期时间的目录中。 这比看起来更难,因为您不知道秘密使用日期时间的所有位置。
使用策略。 将代码中对
datetime.date.today()
和datetime.date.now()
的显式引用替换为生成这些内容的Factory。 工厂必须由应用程序(或单元测试)配置模块。 此配置(有些人称为“依赖注入”)允许您用特殊的测试工厂替换正常的运行时工厂。 您无需对生产进行特殊情况处理即可获得很大的灵活性。 没有“如果测试采取不同的方式”的业务。这是策略版本。
现在您可以执行此操作
从长远来看,您或多或少必须执行此操作,以将浏览器区域设置与服务器区域设置分开。 使用默认的
datetime.datetime.now()
使用服务器的区域设置,这可能会激怒位于不同时区的用户。Two choices.
Mock out datetime by providing your own. Since the local directory is searched before the standard library directories, you can put your tests in a directory with your own mock version of datetime. This is harder than it appears, because you don't know all the places datetime is secretly used.
Use Strategy. Replace explicit references to
datetime.date.today()
anddatetime.date.now()
in your code with a Factory that generates these. The Factory must be configured with the module by the application (or the unittest). This configuration (called "Dependency Injection" by some) allows you to replace the normal run-time Factory with a special test factory. You gain a lot of flexibility with no special case handling of production. No "if testing do this differently" business.Here's the Strategy version.
Now you can do this
In the long run, you more-or-less must do this to account for browser locale separate from server locale. Using default
datetime.datetime.now()
uses the server's locale, which may piss off users who are in a different time zone.