Групповое тестирование 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()
, создав Событие, которое заканчивается в будущем (сегодня + 1 или что-то), и завершая дату и время, чтобы система думала, что мы достигли этой будущей даты.
Я хотел бы иметь возможность заглушить ВСЕ объекты системного времени до python. Это включает datetime.date.today()
, datetime.datetime.now()
и любые другие стандартные объекты даты/времени.
Какой стандартный способ сделать это?
Ответы
Ответ 1
EDIT. Поскольку мой ответ является принятым ответом, я обновляю его, чтобы все знали, что лучший способ был создан тем временем, библиотека freezegun: https://pypi.python.org/pypi/freezegun. Я использую это во всех своих проектах, когда хочу повлиять на время в тестах. Посмотрите на это.
Оригинальный ответ:
Замена внутренних вещей, подобных этому, всегда опасна, поскольку она может иметь неприятные побочные эффекты. Так что вы действительно хотите, чтобы патч обезьяны был максимально локальным.
Мы используем отличную mock-библиотеку 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()
Затем мы создаем unittest для этого валидатора в 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 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 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.'))
Надеюсь, что это поможет
Ответ 2
Вы можете написать свой собственный класс замены модуля даты и времени, внедряя методы и классы из даты и времени, которые вы хотите заменить. Например:
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]
будет использоваться вместо этого.
Ответ 3
Небольшое отклонение от решения Steef. Вместо замены datetime в глобальном масштабе вместо этого вы можете просто заменить модуль datetime только в том модуле, который вы тестируете, например:
import models # your module with the Event model
import datetimestub
models.datetime = datetimestub.DatetimeStub()
Таким образом, изменение во время теста намного локализовано.
Ответ 4
Я бы предложил взглянуть на testfixtures test_datetime().
Ответ 5
Что, если вы издеваетесь над self.end_date вместо datetime? Затем вы можете проверить, что функция делает то, что вы хотите, без всех других сумасшедших обходных решений.
Это не позволит вам заглушить все дату/время, как первоначально спрашивает ваш вопрос, но это может быть не совсем необходимо.
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())
Ответ 6
Это не позволяет заменить системную дату-дату, но если вам надоело пытаться заставить что-то работать, вы всегда можете добавить необязательный параметр, чтобы упростить его тестирование.
def is_over(self, today=datetime.datetime.now()):
return today > self.date_end
Ответ 7
Два варианта.
-
Выделите дату и время, предоставив свои собственные. Поскольку поиск в локальном каталоге выполняется перед стандартными библиотечными каталогами, вы можете поместить свои тесты в каталог с вашей собственной макетной версией datetime. Это сложнее, чем кажется, потому что вы не знаете все места, которые тайное время тайно используется.
-
Используйте Стратегия. Замените явные ссылки на datetime.date.today()
и datetime.date.now()
в вашем коде с помощью Factory, который их генерирует. Factory должен быть настроен с помощью модуля с помощью приложения (или unittest). Эта конфигурация (называемая "Injection Dependency" некоторыми) позволяет заменить обычное время выполнения Factory специальным тестом factory. Вы получаете большую гибкость при отсутствии специальных операций с производством. Нет "если тестирование делает это по-другому".
Здесь Стратегия.
class DateTimeFactory( object ):
"""Today and now, based on server defined locale.
A subclass may apply different rules for determining "today".
For example, the broswer time-zone could be used instead of the
server 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()
использует локаль сервера, которая может вызывать пользователей, которые находятся в другом часовом поясе.