2009-06-25 9 views
23

Supponiamo che io ho il seguente Event modello:Django unit testing con data/oggetti basati sul tempo

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 

Voglio testare Event.is_over() con la creazione di un evento che termina in futuro (oggi + 1 o qualcosa del genere), e stubing la data e l'ora in modo che il sistema pensi che abbiamo raggiunto quella data futura.

Mi piacerebbe essere in grado di mozzare TUTTI gli oggetti del tempo di sistema per quanto riguarda Python. Ciò include datetime.date.today(), datetime.datetime.now() e qualsiasi altro oggetto di data/ora standard.

Qual è il modo standard per farlo?

risposta

29

EDIT: Dal momento che la mia risposta è la risposta accettata qui sto aggiornando a far sapere a tutti un modo migliore è stata creata nel frattempo, la biblioteca freezegun: https://pypi.python.org/pypi/freezegun. Lo uso in tutti i miei progetti quando voglio influenzare il tempo nei test. Date un'occhiata a questo.

risposta originale:

Sostituzione roba interna come questo è sempre pericoloso perché può avere effetti collaterali sgradevoli. Quindi, quello che vuoi davvero è avere la patch della scimmia più locale possibile.

Usiamo eccellente biblioteca finta di Michael Foord: http://www.voidspace.org.uk/python/mock/ che ha un decoratore @patch quali patch alcune funzionalità, ma la patch scimmia vive solo nel campo di applicazione della funzione di prova, e tutto ciò che viene ripristinato automaticamente dopo la funzione esaurisce la sua portata .

L'unico problema è che il modulo interno datetime è implementato in C, quindi per impostazione predefinita non sarà possibile eseguire il patch della patch. Abbiamo risolto questo problema rendendo la nostra implementazione semplice che può essere derisa.

La soluzione totale è qualcosa di simile (l'esempio è una funzione di convalida utilizzata all'interno di un progetto Django per convalidare che una data è in futuro). Tenete conto che ho preso questo da un progetto ma ho eliminato le cose non importanti, quindi le cose potrebbero non funzionare quando copia-incolla questo, ma l'idea, spero :)

Per prima cosa definiamo il nostro molto semplice realizzazione di datetime.date.today in un file chiamato utils/date.py:

import datetime 

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

Poi creiamo l'unittest per questo validatore in 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 realizzazione finale è simile al seguente:

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

Spero che questo aiuti

+0

Questo è davvero il modo giusto.Questa libreria fittizia è superba. – hendrixski

+1

Ho scritto un metodo alternativo utilizzare mock/patch con datetime: http: //www.voidspace.org.uk/python/weblog/arch_d7_2010_10_02.shtml#e1188 – fuzzyman

-5

Due scelte.

  1. Derivare datetime fornendo il proprio. Poiché la directory locale viene ricercata prima delle directory della libreria standard, è possibile inserire i test in una directory con la propria versione fittizia di datetime. Questo è più difficile di quanto sembri, perché non conosci tutti i luoghi in cui viene usato segretamente datetime.

  2. Utilizzare Strategia. Sostituisci i riferimenti espliciti a datetime.date.today() e datetime.date.now() nel tuo codice con uno Factory che li genera. La fabbrica Factory deve essere configurata con il modulo dall'applicazione (o dall'unità non più richiesta). Questa configurazione (chiamata "Iniezione delle dipendenze" di alcuni) consente di sostituire il normale tempo di esecuzione Factory con un factory di prova speciale. Ottieni molta flessibilità senza la gestione di casi particolari della produzione. No "se il test fa diversamente".

Ecco il strategia versione.

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 

Ora si può fare questo

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

Nel lungo periodo, è più o meno deve fare questo per tenere conto di localizzazione del browser separata dal locale del server. L'utilizzo di default datetime.datetime.now() utilizza le impostazioni locali del server, che possono far incazzare gli utenti che si trovano in un fuso orario diverso.

+0

Non mi piace particolarmente questa soluzione perché comporta la complicazione del codice di produzione per il codice di prova, utilizzando metodi di data/ora non standard. – Fragsworth

+0

(a) Sono chiamate alla funzione datetime.datetime.now() standard. Cosa non è standard? (b) Tutti i progetti dovrebbero consentire la Strategia (o (b) Iniezione delle Dipendenze) perché è così che UnitTesting (e architettura) viene fatto bene. –

+0

Questo potrebbe funzionare per un sistema che si costruisce da zero, ma quando si estraggono più librerie di terze parti (ciascuna chiamata all'origine datetime.datetime.now()) può diventare un problema di manutenzione. Vorrei ridurre al minimo la quantità di codice di libreria di terze parti che devo modificare, quindi una soluzione che cambi i risultati dei metodi Python originali sarebbe l'ideale. – Fragsworth

7

È possibile scrivere la propria classe di sostituzione del modulo datetime, implementando i metodi e le classi da datetime che si desidera sostituire. Per esempio:

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) 

Mettiamo questo nel proprio modulo che chiameremo datetimestub.py

Poi, all'inizio del test, si può fare questo:

import sys 
import datetimestub 

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

Qualsiasi importazione successiva del modulo datetime verrà quindi utilizzata l'istanza datetimestub.DatetimeStub, poiché quando il nome di un modulo viene utilizzato come chiave nel dizionario sys.modules, il modulo non verrà importato: verrà invece utilizzato l'oggetto su sys.modules[module_name].

6

Leggera variazione rispetto alla soluzione di Steef. Piuttosto che sostituire datetime a livello globale invece si può solo sostituire il modulo datetime in un solo modulo che si sta testando, per es .:

 

import models # your module with the Event model 
import datetimestub 

models.datetime = datetimestub.DatetimeStub() 
 

In questo modo il cambiamento è molto più localizzata durante il test.

+0

Oppure, forse "importa mockdatetime come datetime"? –

+2

Questo implicherebbe la modifica del codice che stavi testando, vero? Tutto ciò che si vuole veramente fare è re-associare il nome "datetime" nel modulo dei modelli. –

+0

Alla fine della giornata si tratta di sfruttare la natura dinamica di Python per evitare di dover complicare inutilmente il codice. –

1

Questo non esegue la sostituzione datetime a livello di sistema, ma se sei stufo di provare a far funzionare qualcosa, puoi sempre aggiungere un parametro opzionale per renderlo più facile per il test.

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

Non sono sicuro che funzionerà su un thread di lunga durata come quello che avresti in un ambiente mod_wsgi django +. Penso che il default oggi verrà compilato la prima volta che il tuo programma viene caricato e poi rimane lo stesso fino alla prossima volta che il codice è stato ricaricato. – Aaron

+0

Grazie, ho provato ad aggiornarlo per renderlo conto. – monkut

+1

è possibile ridurre ulteriormente: def is_over (self, today = datetime.datetime.now(): ritorno oggi> self.date_end –

3

cosa succede se si deriso la self.end_date invece del datetime? Quindi potresti ancora verificare che la funzione stia facendo quello che vuoi senza tutte le altre soluzioni pazze suggerite.

Questo non consente di bloccare tutte le date/ore come inizialmente richiesto dalla domanda, ma potrebbe non essere completamente necessario.

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