python 使用基于日期/时间的对象进行 Django 单元测试
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1042900/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Django unit testing with date/time-based objects
提问by Fragsworth
Suppose I have the following Event
model:
假设我有以下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
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.
我想Event.is_over()
通过创建一个在未来(今天 + 1 或其他)结束的事件进行测试,并存根日期和时间,以便系统认为我们已经达到了未来的日期。
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.
就python而言,我希望能够存根所有系统时间对象。这包括datetime.date.today()
、datetime.datetime.now()
和任何其他标准日期/时间对象。
What's the standard way to do this?
执行此操作的标准方法是什么?
回答by Remco Wendt
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.
编辑:由于我的答案是这里接受的答案,因此我正在更新它以让每个人都知道在此期间创建了更好的方法,即 freezegun 库:https://pypi.python.org/pypi/freezegun 。当我想影响测试时间时,我会在所有项目中使用它。看看它。
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.
我们使用 Michael Foord 的优秀模拟库:http://www.voidspace.org.uk/python/mock/ ,它有一个@patch
装饰器可以修补某些功能,但猴子补丁只存在于测试功能的范围内,一切都是函数用完后自动恢复。
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 canbe mocked.
唯一的问题是内部datetime
模块是用 C 实现的,所以默认情况下你将无法修补它。我们通过制作我们自己的可以模拟的简单实现来解决这个问题。
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 :)
整个解决方案是这样的(示例是在 Django 项目中使用的验证器函数,用于验证日期是否在未来)。请注意,我从一个项目中取出了这个,但去掉了不重要的东西,所以复制粘贴这个时,事情可能实际上不起作用,但你明白了,我希望:)
First we define our own very simple implementation of datetime.date.today
in a file called utils/date.py
:
首先,我们datetime.date.today
在一个名为 的文件中定义我们自己的非常简单的实现utils/date.py
:
import datetime
def today():
return datetime.date.today()
Then we create the unittest for this validator in tests.py
:
然后我们为这个验证器创建单元测试tests.py
:
import datetime
import mock
from unittest2 import TestCase
from django.core.exceptions import ValidationError
from .. import validators
class ValidationTests(TestCase):
@mock.patch('utils.date.today')
def test_validate_future_date(self, today_mock):
# Pin python's today to returning the same date
# always so we can actually keep on unit testing in the future :)
today_mock.return_value = datetime.date(2010, 1, 1)
# A future date should work
validators.validate_future_date(datetime.date(2010, 1, 2))
# The mocked today's date should fail
with self.assertRaises(ValidationError) as e:
validators.validate_future_date(datetime.date(2010, 1, 1))
self.assertEquals([u'Date should be in the future.'], e.exception.messages)
# Date in the past should also fail
with self.assertRaises(ValidationError) as e:
validators.validate_future_date(datetime.date(2009, 12, 31))
self.assertEquals([u'Date should be in the future.'], e.exception.messages)
The final implementation looks like this:
最终的实现如下所示:
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from utils import date
def validate_future_date(value):
if value <= date.today():
raise ValidationError(_('Date should be in the future.'))
Hope this helps
希望这可以帮助
回答by Steef
You could write your own datetime module replacement class, implementing the methods and classes from datetime that you want to replace. For example:
您可以编写自己的 datetime 模块替换类,从要替换的 datetime 中实现方法和类。例如:
import datetime as datetime_orig
class DatetimeStub(object):
"""A datetimestub object to replace methods and classes from
the datetime module.
Usage:
import sys
sys.modules['datetime'] = DatetimeStub()
"""
class datetime(datetime_orig.datetime):
@classmethod
def now(cls):
"""Override the datetime.now() method to return a
datetime one year in the future
"""
result = datetime_orig.datetime.now()
return result.replace(year=result.year + 1)
def __getattr__(self, attr):
"""Get the default implementation for the classes and methods
from datetime that are not replaced
"""
return getattr(datetime_orig, attr)
Let's put this in its own module we'll call datetimestub.py
让我们把它放在我们将调用的自己的模块中 datetimestub.py
Then, at the start of your test, you can do this:
然后,在测试开始时,您可以执行以下操作:
import sys
import datetimestub
sys.modules['datetime'] = datetimestub.DatetimeStub()
Any subsequent import of the datetime
module will then use the datetimestub.DatetimeStub
instance, because when a module's name is used as a key in the sys.modules
dictionary, the module will not be imported: the object at sys.modules[module_name]
will be used instead.
datetime
模块的任何后续导入都将使用该datetimestub.DatetimeStub
实例,因为当模块的名称用作sys.modules
字典中的键时,将不会导入该模块:sys.modules[module_name]
将使用对象 at代替。
回答by John Montgomery
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.:
Steef 的解决方案略有不同。而不是全局替换 datetime 而是你可以只替换你正在测试的模块中的 datetime 模块,例如:
import models # your module with the Event model
import datetimestub
models.datetime = datetimestub.DatetimeStub()
That way the change is much more localised during your test.
这样,在您的测试期间,更改会更加本地化。
回答by Chris Withers
I'd suggest taking a look at testfixturestest_datetime().
我建议看看testfixtures test_datetime()。
回答by Aaron
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.
如果你嘲笑 self.end_date 而不是日期时间怎么办?然后您仍然可以测试该函数是否正在执行您想要的操作,而无需建议所有其他疯狂的解决方法。
This wouldn't let you stub all date/times like your question initially asks, but that might not be completely necessary.
这不会让您像您的问题最初提出的那样存根所有日期/时间,但这可能不是完全必要的。
today = datetime.date.today() event1 = Event() event1.end_date = today - datetime.timedelta(days=1) # 1 day ago event2 = Event() event2.end_date = today + datetime.timedelta(days=1) # 1 day in future self.assertTrue(event1.is_over()) self.assertFalse(event2.is_over())
回答by monkut
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.
这不会执行系统范围的日期时间替换,但是如果您厌倦了尝试让某些东西工作,您可以随时添加一个可选参数以使其更易于测试。
def is_over(self, today=datetime.datetime.now()):
return today > self.date_end
回答by S.Lott
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 Factorythat generates these. The Factorymust 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 Factorywith a special test factory. You gain a lot of flexibility with no special case handling of production. No "if testing do this differently" business.
通过提供您自己的日期时间来模拟日期时间。由于在标准库目录之前搜索本地目录,您可以将您的测试放在具有您自己的 datetime 模拟版本的目录中。这比看起来更难,因为您不知道暗中使用 datetime 的所有地方。
使用策略。用生成这些的工厂替换对代码中
datetime.date.today()
和datetime.date.now()
代码中的显式引用。的工厂必须与由应用程序(或单元测试)的模块来构成。这种配置(称为“依赖注入”一些),可以取代正常运行时工厂有专门的测试工厂。您获得了很大的灵活性,无需对生产进行特殊处理。没有“如果测试以不同的方式这样做”的业务。
Here's the Strategyversion.
这是策略版本。
class DateTimeFactory( object ):
"""Today and now, based on server's defined locale.
A subclass may apply different rules for determining "today".
For example, the broswer's time-zone could be used instead of the
server's timezone.
"""
def getToday( self ):
return datetime.date.today()
def getNow( self ):
return datetime.datetime.now()
class Event( models.Model ):
dateFactory= DateTimeFactory() # Definitions of "now" and "today".
... etc. ...
def is_over( self ):
return dateFactory.getToday() > self.date_end
class DateTimeMock( object ):
def __init__( self, year, month, day, hour=0, minute=0, second=0, date=None ):
if date:
self.today= date
self.now= datetime.datetime.combine(date,datetime.time(hour,minute,second))
else:
self.today= datetime.date(year, month, day )
self.now= datetime.datetime( year, month, day, hour, minute, second )
def getToday( self ):
return self.today
def getNow( self ):
return self.now
Now you can do this
现在你可以这样做
class SomeTest( unittest.TestCase ):
def setUp( self ):
tomorrow = datetime.date.today() + datetime.timedelta(1)
self.dateFactoryTomorrow= DateTimeMock( date=tomorrow )
yesterday = datetime.date.today() + datetime.timedelta(1)
self.dateFactoryYesterday= DateTimeMock( date=yesterday )
def testThis( self ):
x= Event( ... )
x.dateFactory= self.dateFactoryTomorrow
self.assertFalse( x.is_over() )
x.dateFactory= self.dateFactoryYesterday
self.asserTrue( x.is_over() )
In the long run, you more-or-less mustdo 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.
从长远来看,您或多或少必须这样做才能将浏览器区域设置与服务器区域设置分开。使用默认值会datetime.datetime.now()
使用服务器的区域设置,这可能会惹恼不同时区的用户。