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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 21:21:02  来源:igfitidea点击:

Django unit testing with date/time-based objects

pythondjangounit-testingdatetimestub

提问by Fragsworth

Suppose I have the following Eventmodel:

假设我有以下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 @patchdecorator 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 datetimemodule 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.todayin 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 datetimemodule will then use the datetimestub.DatetimeStubinstance, because when a module's name is used as a key in the sys.modulesdictionary, 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.

两个选择。

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

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

  2. 使用策略。用生成这些的工厂替换对代码中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()使用服务器的区域设置,这可能会惹恼不同时区的用户。