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.is_over()
这个功能,方法是创建一个在未来结束的事件(比如今天加一天),然后把日期和时间进行“伪装”,让系统以为我们已经到了那个未来的日期。
我希望能够在 Python 中伪装所有的系统时间对象。这包括 datetime.date.today()
、datetime.datetime.now()
以及其他任何标准的日期/时间对象。
这样做的标准方法是什么呢?
7 个回答
这是对Steef解决方案的一个小改动。与其在全局范围内替换datetime,不如只在你正在测试的模块中替换datetime模块,比如:
import models # your module with the Event model
import datetimestub
models.datetime = datetimestub.DatetimeStub()
这样在测试期间的改动就更局部,更容易控制。
你可以自己写一个替代的日期时间模块类,来实现你想替换的日期时间模块中的方法和类。例如:
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)
我们把这个放在一个单独的模块里,叫做 datetimestub.py
然后,在你测试的开始部分,你可以这样做:
import sys
import datetimestub
sys.modules['datetime'] = datetimestub.DatetimeStub()
之后任何对 datetime
模块的导入都会使用 datetimestub.DatetimeStub
实例。因为当一个模块的名字作为键在 sys.modules
字典中使用时,这个模块就不会被重新导入:而是会使用 sys.modules[module_name]
中的对象。
编辑:因为我的回答被接受为最佳答案,所以我来更新一下,告诉大家现在有了一个更好的方法,那就是使用 freezegun 库:https://pypi.python.org/pypi/freezegun。我在所有项目中都使用这个库,当我想在测试中控制时间时。可以去看看。
原始回答:
像这样替换内部功能总是有风险,因为可能会引发一些意想不到的问题。所以你真正想要的是让这种“猴子补丁”尽可能局部。
我们使用 Michael Foord 的优秀 mock 库:http://www.voidspace.org.uk/python/mock/,这个库有一个 @patch
装饰器,可以修补某些功能,但这个猴子补丁只在测试函数的范围内有效,函数执行完后,所有内容都会自动恢复。
唯一的问题是,内部的 datetime
模块是用 C 语言实现的,所以默认情况下你无法对它进行猴子补丁。我们通过自己实现一个简单的版本来解决这个问题,这个版本是可以被模拟的。
完整的解决方案大致如下(这个例子是一个在 Django 项目中用于验证日期是否在未来的验证函数)。请注意,我是从一个项目中提取的,但去掉了一些不重要的部分,所以直接复制粘贴可能不太能工作,但我希望你能明白这个思路 :)
首先,我们在一个名为 utils/date.py
的文件中定义我们自己的非常简单的 datetime.date.today
实现:
import datetime
def today():
return datetime.date.today()
然后我们在 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)
最终的实现看起来是这样的:
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.'))
希望这能帮到你