Django对基于日期/时间对象的单元测试

38 投票
7 回答
30952 浏览
提问于 2025-04-15 12:29

假设我有一个叫做 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 个回答

7

这是对Steef解决方案的一个小改动。与其在全局范围内替换datetime,不如只在你正在测试的模块中替换datetime模块,比如:


import models # your module with the Event model
import datetimestub

models.datetime = datetimestub.DatetimeStub()

这样在测试期间的改动就更局部,更容易控制。

7

你可以自己写一个替代的日期时间模块类,来实现你想替换的日期时间模块中的方法和类。例如:

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] 中的对象。

43

编辑:因为我的回答被接受为最佳答案,所以我来更新一下,告诉大家现在有了一个更好的方法,那就是使用 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.'))

希望这能帮到你

撰写回答