Групповое тестирование 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()

Таким образом, изменение во время теста намного локализовано.

Ответ 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() использует локаль сервера, которая может вызывать пользователей, которые находятся в другом часовом поясе.