使用基于日期/时间的对象进行 Django 单元测试

发布于 2024-07-25 00:06:51 字数 546 浏览 5 评论 0原文

假设我有以下 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

假装不在乎 2024-08-01 00:06:51

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

希望这有帮助

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 called utils/date.py:

import datetime

def today():
    return datetime.date.today()

Then we create the unittest for this validator in 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

秋凉 2024-08-01 00:06:51

您可以编写自己的日期时间模块替换类,实现要替换的日期时间中的方法和类。 例如:

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

You could write your own datetime module replacement class, implementing the methods and classes from datetime that you want to replace. For example:

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

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.

厌味 2024-08-01 00:06:51

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


import models # your module with the Event model
import datetimestub

models.datetime = datetimestub.DatetimeStub()

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

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.:


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.

自由范儿 2024-08-01 00:06:51

我建议看看 testfixtures test_datetime()

I'd suggest taking a look at testfixtures test_datetime().

梦言归人 2024-08-01 00:06:51

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

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

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

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.

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())
一腔孤↑勇 2024-08-01 00:06:51

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

def is_over(self, today=datetime.datetime.now()):
    return today > self.date_end

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
燕归巢 2024-08-01 00:06:51

两个选择。

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

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

这是策略版本。

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() 使用服务器的区域设置,这可能会激怒位于不同时区的用户。

Two choices.

  1. 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.

  2. Use Strategy. Replace explicit references to datetime.date.today() and datetime.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.

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文