2010-04-17 8 views
18

Cuando escribo con lógica comercial, mi código a menudo depende de la hora actual. Por ejemplo, el algoritmo que examina cada orden pendiente y verifica si se debe enviar una factura (que depende del número de días desde que finalizó el trabajo). En estos casos, la creación de una factura no se desencadena por una acción explícita del usuario, sino por un trabajo en segundo plano.¿Cómo cambiar la fecha/hora en Python para todos los módulos?

Ahora bien, esto crea un problema para mí cuando se trata de pruebas:

  • puedo prueba en sí misma creación de la factura fácil
  • Sin embargo, es difícil crear un orden en una prueba y comprobar que el trabajo en segundo plano identifica los pedidos correctos en el momento correcto.

Hasta ahora he encontrado dos soluciones:

  • En la configuración de la prueba, calcular las fechas de trabajo en relación con la fecha actual. Desventaja: el código se vuelve bastante complicado ya que no hay fechas explícitas escritas. En ocasiones, la lógica comercial es bastante compleja para los casos límite, por lo que es difícil realizar la depuración debido a todas estas fechas relativas.
  • Tengo mis propias funciones de acceso de fecha/hora que utilizo a lo largo de mi código. En la prueba, simplemente establezco una fecha actual y todos los módulos obtienen esta fecha. De modo que puedo simular una creación de orden en febrero y verificar que la factura se cree en abril fácilmente. Desventaja: los módulos de terceros no usan este mecanismo, por lo que es realmente difícil integrarlos + probarlos.

El segundo enfoque fue mucho más exitoso para mí, después de todo. Por lo tanto, estoy buscando una manera de establecer el tiempo de regreso de los módulos datetime + time de Python. La configuración de la fecha suele ser suficiente, no es necesario configurar la hora o el segundo (aunque esto sería bueno).

¿Existe tal utilidad? ¿Hay una API (interna) de Python que pueda usar?

Respuesta

8

Monkey-patching time.time es probablemente suficiente, de hecho, ya que proporciona la base para casi todas las otras rutinas basadas en tiempo en Python. Esto parece manejar su caso de uso bastante bien, sin recurrir a trucos más complejos, y no importa cuando lo hace (aparte de los pocos paquetes stdlib como Queue.py y threading.py que hacen from time import time en cuyo caso debe parche antes de que se importan):

>>> import datetime 
>>> datetime.datetime.now() 
datetime.datetime(2010, 4, 17, 14, 5, 35, 642000) 
>>> import time 
>>> def mytime(): return 120000000.0 
... 
>>> time.time = mytime 
>>> datetime.datetime.now() 
datetime.datetime(1973, 10, 20, 17, 20) 

dicho esto, en los años de burlarse de objetos para varios tipos de pruebas automatizadas, que he necesitado este enfoque sólo en muy raras ocasiones, como la mayoría de las veces es mi propio código de aplicación que necesita la burla, y no las rutinas stdlib. Después de todo, ya sabes que ya funcionan. Si se encuentra con situaciones en las que su propio código tiene que manejar los valores devueltos por las rutinas de la biblioteca, es posible que desee burlarse de las rutinas de la biblioteca, al menos cuando verifique cómo su propia aplicación manejará las marcas de tiempo.

El mejor enfoque es construir sus propias rutinas de servicio de fecha/hora que utiliza exclusivamente en su código de aplicación, y incorporar la posibilidad de que las pruebas proporcionen resultados falsos según sea necesario. Por ejemplo, hago una más compleja equivalente de esto a veces:

# in file apptime.py (for example) 
import time as _time 

class MyTimeService(object): 
    def __init__(self, get_time=None): 
     self.get_time = get_time or _time.time 

    def __call__(self): 
     return self.get_time() 

time = MyTimeService() 

Ahora en mi código de aplicación acabo de hacer import apptime as time; time.time() para obtener el valor de la hora actual, mientras que en el código de prueba que puedo hacer primero apptime.time = MyTimeService(mock_time_func) en mi código setUp() a suministrar resultados de tiempo falsos.

Actualización: Años después hay una alternativa, como se indica en Dave Forgac's answer.

+7

¿Qué versión de Python probé intente esto? Acabo de probar 2.6 en MacOS 10.6 y datetime.datetime.now() es felizmente inconsciente del cambio en time.time –

+0

Malcolm, acabo de probar nuevamente en Python 2.7.1 en Windows, y funciona exactamente como se muestra arriba . Tal vez depende de la plataforma, lo que no sería del todo sorprendente, supongo. –

+4

Probé esto en Ubuntu 12.04 con Python 2.7.3, y no funciona. Después de parchear mono 'time.time' con' mytime' como se describe, la llamada 'datetime.datetime.now' aún devuelve la hora actual. – djsmith

0

podría haber algunas formas de hacerlo, como crear los pedidos (con la marca de tiempo actual) y luego cambiar ese valor en el DB directamente por algún proceso externo (suponiendo que los datos estén en el DB).

Voy a sugerir algo más. ¿Ha pensado en ejecutar su aplicación en una máquina virtual, establecer el horario para decir feb, crear pedidos y luego simplemente cambiar el tiempo de las máquinas virtuales? Este enfoque es lo más cercano que se puede llegar a la situación de la vida real.

+0

Sí, las fechas que se mueven en la base de datos directamente podría funcionar. Sin embargo, a veces esto no es factible, especialmente si tiene muchos campos de fecha en varias tablas (por ejemplo, cada elemento tiene marcas de tiempo para crear, cambió + el historial completo). Así que cambiar todos estos podría ser una gran molestia. Para las máquinas virtuales: No voy a ir allí, ya que aumentará la complejidad de las pruebas en algunos órdenes de magnitud -> es mucho más probable que la prueba falle, las pruebas se ejecuten más despacio (¡las pruebas rápidas son extremadamente importantes para mí!) esfuerzo para integrar eso en mis servidores de compilación. –

1

Bueno, una forma de hacerlo es a parche dinámico del módulo de hora/fecha y hora

algo así como

import time 
import datetime 

class MyDatetime: 

    def now(self): 
     return time.time() 

datetime.datetime = MyDatetime 

print datetime.datetime().now() 
+1

Sí, eso podría funcionar; sin embargo, es demasiado frágil porque necesito aplicar un parche a todos los módulos externos: si un módulo externo usa 'desde datetime import datetime', el parche de mono que se muestra arriba no tendrá ningún efecto. –

+1

@Felix, tendrá un efecto si aplica un parche al módulo de fecha y hora antes de importar cualquier otra cosa. Si planeas hacer parches de mono (que no es una solución elegante, pero puede ser pragmática), entonces tienes que vivir con tanta torpeza. –

2

Usted puede parchear el sistema, mediante la creación de un módulo de fecha y hora personalizado (incluso una falsificación uno - vea el ejemplo a continuación) actuando como un proxy y luego insertarlo en el diccionario sys.modules. De allí en adelante, cada importación al módulo de fecha y hora devolverá su proxy.
Todavía existe la advertencia de la clase de fecha y hora, especialmente cuando alguien hace from datetime import datetime; para eso, simplemente puede agregar otro proxy solo para esa clase.

Aquí hay un ejemplo de lo que estoy diciendo: por supuesto, es algo que he lanzado en 5 minutos y puede tener varios problemas (por ejemplo, el tipo de clase datetime no es correcto); pero con suerte ya puede ser útil.

import sys 
import datetime as datetime_orig 

class DummyDateTimeModule(sys.__class__): 
    """ Dummy class, for faking datetime module """ 
    def __init__(self): 
    sys.modules["datetime"] = self 
    def __getattr__(self, attr): 
    if attr=="datetime": 
     return DummyDateTimeClass() 
    else: 
     return getattr(datetime_orig, attr) 

class DummyDateTimeClass(object): 
    def __getattr__(self, attr): 
    return getattr(datetime_orig.datetime, attr) 

dt_fake = DummyDateTimeModule() 

Finalmente, ¿vale la pena?
Francamente, me gusta nuestra segunda solución mucho más que esta :-).
Sí, Python es un lenguaje muy dinámico, donde puedes hacer un montón de cosas interesantes, pero el código de parcheo de esta manera siempre tiene un cierto grado de riesgo, incluso si estamos hablando aquí de código de prueba.
Pero sobre todo, creo que la función de accesorios haría que el parche de prueba sea más explícito, y también su código sería más explícito en términos de lo que se va a probar, aumentando así la legibilidad.
Por lo tanto, si el cambio no es demasiado caro, elegiría su segundo enfoque.

4

El paquete freezegun fue hecho específicamente para este propósito. Le permite cambiar la fecha del código bajo prueba. Se puede usar directamente o a través de un decorador o administrador de contexto. Un ejemplo:

from freezegun import freeze_time 
import datetime 

@freeze_time("2012-01-14") 
def test(): 
    assert datetime.datetime.now() == datetime.datetime(2012, 1, 14) 

Para ver más ejemplos del proyecto: https://github.com/spulec/freezegun

+2

¡Esa parece ser la mejor opción en estos días! Lamentablemente, el proyecto se creó solo unos pocos años tarde, pero lo usaré en el futuro :-) –

+0

De hecho, esta es la mejor solución. –

Cuestiones relacionadas