Юнит-тестирование Django с объектами на основе даты / времени - PullRequest
25 голосов
/ 25 июня 2009

Предположим, у меня есть 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() и любые другие стандартные объекты даты / времени.

Какой стандартный способ сделать это?

Ответы [ 7 ]

31 голосов
/ 01 июля 2010

РЕДАКТИРОВАТЬ : Так как мой ответ является принятым ответом здесь, я обновляю его, чтобы все знали, что тем временем был создан лучший способ, библиотека freezegun: https://pypi.python.org/pypi/freezegun. Я использую это во всех моих проектах, когда я хочу влиять на время в тестах. Посмотрите на это.

Оригинальный ответ:

Замена таких внутренних вещей всегда опасна, поскольку может иметь неприятные побочные эффекты. Так что вы действительно хотите, чтобы патч обезьяны был как можно более локальным.

Мы используем превосходную библиотеку-макет Майкла Фоорда: 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.'))

Надеюсь, это поможет

7 голосов
/ 25 июня 2009

Вы можете написать свой собственный класс замены модуля 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] будет использоваться вместо.

6 голосов
/ 25 июня 2009

Небольшое отклонение от решения Стифа. Вместо того, чтобы заменять datetime глобально, вместо этого вы можете просто заменить модуль datetime только тем модулем, который вы тестируете, например ::10000


import models # your module with the Event model
import datetimestub

models.datetime = datetimestub.DatetimeStub()

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

4 голосов
/ 04 августа 2011

Я бы посоветовал взглянуть на testfixtures test_datetime () .

3 голосов
/ 25 сентября 2010

Что делать, если вы издевались над 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())
1 голос
/ 26 июня 2009

Это не выполняет общесистемную замену даты и времени, но если вам надоело пытаться заставить что-то работать, вы всегда можете добавить необязательный параметр, чтобы облегчить его тестирование.

def is_over(self, today=datetime.datetime.now()):
    return today > self.date_end
0 голосов
/ 25 июня 2009

Два варианта.

  1. Смоделируйте дату и время, предоставив свои собственные. Так как локальный каталог ищется перед каталогами стандартной библиотеки, вы можете поместить свои тесты в каталог с вашей собственной фиктивной версией datetime. Это сложнее, чем кажется, потому что вы не знаете всех мест, где тайно используется дата и время.

  2. Использование Стратегия . Замените явные ссылки на datetime.date.today() и datetime.date.now() в вашем коде на Factory , который генерирует их. Factory должен быть сконфигурирован с модулем приложением (или тестированием модуля). Эта конфигурация (в некоторых случаях называемая «Внедрение зависимостей») позволяет заменить обычное время выполнения 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() используется языковой стандарт сервера, что может раздражать пользователей, находящихся в другом часовом поясе.

...