Pruebas unitarias de Django con objetos basados ​​en fecha / hora

Supongamos que tengo el siguiente modelo de 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 

Quiero probar Event.is_over() creando un Evento que finalice en el futuro (hoy +1 o algo), y apagué la fecha y la hora para que el sistema piense que hemos llegado a esa fecha futura.

Me gustaría poder detener TODOS los objetos de tiempo del sistema en lo que respecta a python. Esto incluye datetime.date.today() , datetime.datetime.now() y cualquier otro objeto estándar de fecha / hora.

¿Cuál es la forma estándar de hacer esto?

EDITAR : Ya que mi respuesta es la respuesta aceptada aquí, la actualizo para que todos sepan que se ha creado una mejor manera mientras tanto, la biblioteca de freezegun: https://pypi.python.org/pypi/freezegun . Uso esto en todos mis proyectos cuando quiero influir en el tiempo en las pruebas. Échale un vistazo.

Respuesta original:

Reemplazar cosas internas como esta siempre es peligroso porque puede tener efectos secundarios desagradables. Entonces, lo que realmente quieres es que el parche del mono sea lo más local posible.

Utilizamos la excelente biblioteca de simulacros de Michael Foord: http://www.voidspace.org.uk/python/mock/ que tiene un decorador @patch que parchea cierta funcionalidad, pero el parche del mono solo vive en el scope de la función de prueba, y Todo se restaura automáticamente después de que la función se queda fuera de su scope.

El único problema es que el módulo interno de datetime se implementa en C, por lo que de manera predeterminada no podrá parchearlo. Arreglamos esto haciendo nuestra propia implementación simple que puede ser burlada.

La solución total es algo como esto (el ejemplo es una función de validación utilizada en un proyecto de Django para validar que una fecha es en el futuro). Ten en cuenta que tomé esto de un proyecto, pero saqué lo que no era importante, así que las cosas pueden no funcionar al copiar y pegar esto, pero tengo la idea, espero 🙂

Primero definimos nuestra propia implementación muy simple de datetime.date.today en un archivo llamado utils/date.py :

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

Luego creamos la tests.py de tests.py para este validador en 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) 

La implementación final se ve así:

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

Espero que esto ayude

Podría escribir su propia clase de reemplazo de módulo datetime, implementando los métodos y clases a partir de datetime que desee reemplazar. Por ejemplo:

 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) 

Pongamos esto en su propio módulo que llamaremos datetimestub.py

Luego, al inicio de tu prueba, puedes hacer esto:

 import sys import datetimestub sys.modules['datetime'] = datetimestub.DatetimeStub() 

Cualquier importación posterior del módulo datetime utilizará entonces la instancia datetimestub.DatetimeStub , porque cuando el nombre de un módulo se usa como una clave en el diccionario sys.modules , el módulo no se importará: el objeto en sys.modules[module_name] ser utilizado en su lugar.

Variación leve a la solución de Steef. En lugar de reemplazar datetime globalmente, en lugar de eso, simplemente puede reemplazar el módulo datetime solo en el módulo que está probando, por ejemplo:

import models # your module with the Event model import datetimestub models.datetime = datetimestub.DatetimeStub()
import models # your module with the Event model import datetimestub models.datetime = datetimestub.DatetimeStub() 

De esa manera el cambio es mucho más localizado durante su prueba.

Yo sugeriría echar un vistazo a testfixtures test_datetime () .

¿Qué pasaría si se burlara de self.end_date en lugar de datetime? Entonces aún podría probar que la función está haciendo lo que quiere sin todas las otras soluciones locas sugeridas.

Esto no le permitiría anular todas las fechas / horas como su pregunta inicialmente, pero puede que no sea completamente necesario.

 hoy = datetime.date.today ()

 evento1 = Evento ()
 event1.end_date = today - datetime.timedelta (days = 1) hace # 1 día
 evento2 = Evento ()
 event2.end_date = today + datetime.timedelta (days = 1) # 1 día en el futuro

 self.assertTrue (event1.is_over ())
 self.assertFalse (event2.is_over ())

Esto no realiza el reemplazo de la fecha y hora en todo el sistema, pero si está harto de intentar que algo funcione, siempre puede agregar un parámetro opcional para facilitar las pruebas.

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

Dos opciones.

  1. Mock out datetime proporcionando su propio. Como el directorio local se busca antes que los directorios de la biblioteca estándar, puede colocar sus pruebas en un directorio con su propia versión simulada de datetime. Esto es más difícil de lo que parece, porque no conoce todos los lugares que datetime se usa en secreto.

  2. Usa la estrategia . Reemplace las referencias explícitas a datetime.date.today() y datetime.date.now() en su código con una fábrica que las genere. La fábrica debe configurarse con el módulo por la aplicación (o el test de unidad). Esta configuración (denominada “Inyección de dependencia” por algunos) le permite reemplazar la fábrica de tiempo de ejecución normal con una fábrica de prueba especial. Usted gana mucha flexibilidad sin un manejo especial de casos de producción. No “si las pruebas hacen esto de manera diferente” el negocio.

Aquí está la versión de estrategia .

 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 

Ahora puedes hacer esto

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

A la larga, más o menos debe hacer esto para tener en cuenta la configuración regional del navegador separada de la configuración regional del servidor. El uso de datetime.datetime.now() predeterminado usa la configuración regional del servidor, lo que puede molestar a los usuarios que se encuentran en una zona horaria diferente.