35

假设我有以下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()通过创建一个在未来结束的事件(今天 + 1 或其他)进行测试,并存根日期和时间,以便系统认为我们已经达到了未来的日期。

就python而言,我希望能够存根所有系统时间对象。这包括datetime.date.today()datetime.datetime.now()和任何其他标准日期/时间对象。

这样做的标准方法是什么?

4

7 回答 7

39

编辑:由于我的答案是这里公认的答案,我正在更新它,让每个人都知道同时创建了一个更好的方法,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

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.'))

希望这可以帮助

于 2010-07-01T07:38:18.277 回答
7

您可以编写自己的 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)

让我们把它放在它自己的模块中,我们将调用 datetimestub.py

然后,在测试开始时,您可以这样做:

import sys
import datetimestub

sys.modules['datetime'] = datetimestub.DatetimeStub()

任何后续的datetime模块导入都将使用该datetimestub.DatetimeStub实例,因为当模块的名称用作sys.modules字典中的键时,模块将不会被导入:sys.modules[module_name]将使用 at 的对象。

于 2009-06-25T10:23:35.170 回答
7

与 Steef 的解决方案略有不同。而不是全局替换 datetime ,您可以只替换您正在测试的模块中的 datetime 模块,例如:


import models # your module with the Event model
import datetimestub

models.datetime = datetimestub.DatetimeStub()

这样,在您的测试期间更改就更加本地化了。

于 2009-06-25T13:41:46.940 回答
4

我建议看一下testfixtures test_datetime()

于 2011-08-04T12:21:37.960 回答
3

如果你嘲笑 self.end_date 而不是 datetime 怎么办?然后,您仍然可以测试该功能是否正在执行您想要的操作,而无需建议所有其他疯狂的解决方法。

这不会让您像您的问题最初提出的那样对所有日期/时间进行存根,但这可能不是完全必要的。

今天 = datetime.date.today()

事件 1 = 事件()
event1.end_date = today - datetime.timedelta(days=1) # 1 天前
事件2 = 事件()
event2.end_date = today + datetime.timedelta(days=1) # 未来 1 天

self.assertTrue(event1.is_over())
self.assertFalse(event2.is_over())
于 2010-09-24T20:49:54.397 回答
1

这不会执行系统范围的日期时间替换,但如果您厌倦了尝试让某些东西工作,您总是可以添加一个可选参数以使其更易于测试。

def is_over(self, today=datetime.datetime.now()):
    return today > self.date_end
于 2009-06-26T05:11:33.260 回答
-6

两种选择。

  1. 通过提供您自己的来模拟日期时间。由于在标准库目录之前搜索本地目录,因此您可以将测试放在具有您自己的模拟版本 datetime 的目录中。这比看起来更难,因为您不知道秘密使用日期时间的所有地方。

  2. 使用策略。将代码中的datetime.date.today()显式引用替换为生成这些引用的工厂。工厂必须由应用程序(或单元测试)配置模块。这种配置(被某些人称为“依赖注入”)允许您用特殊的测试工厂替换正常的运行时工厂您获得了很大的灵活性,无需对生产进行特殊处理。没有“如果测试以不同的方式进行”的业务。datetime.date.now()

这是策略版本。

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

现在你可以这样做

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() )

从长远来看,您或多或少必须这样做以将浏览器区域设置与服务器区域设置分开。使用默认值datetime.datetime.now()使用服务器的区域设置,这可能会激怒处于不同时区的用户。

于 2009-06-25T10:16:29.083 回答