Cuando trabajas en una función que depende del tiempo ... ¿Cómo organizas las pruebas unitarias? Cuando los escenarios de las pruebas unitarias dependen de la forma en que su programa interpreta "ahora", ¿cómo los configura?
Segunda edición: después de un par de días leyendo tu experiencia
Puedo ver que las técnicas para lidiar con esta situación generalmente giran en torno a uno de estos tres principios:
- Agregue (código duro) una dependencia: agregue una pequeña capa sobre la función / objeto de tiempo, y siempre llame a su función de fecha y hora a través de esta capa. De esta manera, puede obtener el control del tiempo durante los casos de prueba.
- Usa simulacros: tu código permanece exactamente igual. En sus pruebas, reemplaza el objeto de tiempo por un objeto de tiempo falso. A veces, la solución implica modificar el objeto de tiempo genuino que proporciona su lenguaje de programación.
- Use la inyección de dependencia: cree su código para que cualquier referencia de tiempo se pase como parámetro. Luego, tiene el control de los parámetros durante las pruebas.
Las técnicas (o bibliotecas) específicas de un idioma son muy bienvenidas, y se mejorarán si aparece un código ilustrativo. Entonces, lo más interesante es cómo se pueden aplicar principios similares en cualquier plataforma. Y sí ... si puedo aplicarlo de inmediato en PHP, mejor que mejor;)
Comencemos con un ejemplo simple: una aplicación de reserva básica.
Digamos que tenemos API JSON y dos mensajes: un mensaje de solicitud y un mensaje de confirmación. Un escenario estándar es el siguiente:
- Usted hace una solicitud Obtienes una respuesta con un token. El sistema bloquea el recurso que se necesita para cumplir esa solicitud durante 5 minutos.
- Confirma una solicitud, identificada por el token. Si el token se emitió dentro de los 5 minutos, se aceptará (el recurso aún está disponible). Si han pasado más de 5 minutos, debe realizar una nueva solicitud (el recurso fue liberado. Debe verificar su disponibilidad nuevamente).
Aquí viene el escenario de prueba correspondiente:
Hago una solicitud Confirmo (inmediatamente) con el token que recibí. Mi confirmación es aceptada
Hago una solicitud Espero 3 minutos Confirmo con el token que recibí. Mi confirmación es aceptada
Hago una solicitud Espero 6 minutos Confirmo con el token que recibí. Mi confirmación es rechazada.
¿Cómo podemos llegar a programar estas pruebas unitarias? ¿Qué arquitectura debemos usar para que estas funcionalidades sigan siendo comprobables?
Nota de edición : cuando disparamos una solicitud, el tiempo se almacena en una base de datos en un formato que pierde cualquier información sobre milisegundos.
EXTRA - pero quizás un poco detallado: Aquí vienen detalles sobre lo que descubrí haciendo mi "tarea":
Construí mi función con una dependencia de una función de tiempo propia. Mi función VirtualDateTime tiene un método estático get_time () al que llamo donde solía llamar al nuevo DateTime (). Esta función de tiempo me permite simular y controlar qué hora es "ahora", de modo que pueda crear pruebas como: "Configurar ahora para el 21 de enero de 2014 16h15. Hacer una solicitud. Avanzar 3 minutos. Hacer la confirmación". Esto funciona bien, a costa de la dependencia, y "código no tan bonito".
Una solución un poco más integrada sería construir una función myDateTime propia que extienda DateTime con funcionalidades adicionales de "tiempo virtual" (lo más importante, ahora configúrelo como quiero). Esto está haciendo que el código de la primera solución sea un poco más elegante (uso del nuevo myDateTime en lugar del nuevo DateTime), pero termina siendo muy similar: tengo que construir mi característica usando mi propia clase, creando así una dependencia.
Pensé en hackear la función DateTime, para que funcione con mi dependencia cuando la necesito. Sin embargo, ¿hay alguna forma simple y ordenada de reemplazar una clase por otra? (Creo que obtuve una respuesta a eso: ver el espacio de nombres a continuación).
En un entorno PHP, leí que "Runkit" puede permitirme hacer esto (piratear la función DateTime) dinámicamente. Lo que es bueno es que podría verificar que estoy corriendo en un entorno de prueba antes de modificar cualquier cosa sobre DateTime, y dejarlo intacto en producción [1] . Esto suena mucho más seguro y limpio que cualquier pirateo manual de DateTime.
Inyección de dependencia de una función de reloj en cada clase que utiliza el tiempo [2] . ¿No es esto excesivo? Veo que en algunos entornos se vuelve muy útil [5] . Sin embargo, en este caso, no me gusta tanto.
Eliminar la dependencia del tiempo en todas las funciones [2] . ¿Es esto siempre factible? (Ver más ejemplos a continuación)
¿Usando espacios de nombres [2] [3] ? Esto se ve bastante bien. Podría burlarme de DateTime con mi propia función DateTime que extiende \ DateTime ... Use DateTime :: setNow ("2014-01-21 16:15:00") y DateTime :: wait ("+ 3 minutos"). Esto cubre más o menos lo que necesito. ¿Qué pasa si usamos la función time ()? ¿O otras funciones de tiempo? Todavía tengo que evitar su uso en mi código original. O tendría que asegurarme de que cualquier función de tiempo PHP que utilizo en mi código se anule en mis pruebas ... ¿Hay alguna biblioteca disponible que haga exactamente esto?
He estado buscando una manera de cambiar la hora del sistema solo por un hilo. Parece que no hay ninguno [4] . Es una pena: una función PHP "simple" para "Establecer el tiempo hasta el 21 de enero de 2014 16h15 para este hilo" sería una gran característica para este tipo de pruebas.
Cambie la fecha y hora del sistema para la prueba, usando exec (). Esto puede funcionar si no tiene miedo de meterse con otras cosas en el servidor. Y debe retrasar la hora del sistema después de ejecutar su prueba. Puede hacer el truco en algunas situaciones, pero se siente bastante "hacky".
Esto me parece un problema muy estándar. Sin embargo, todavía extraño una forma simple y genérica de tratarlo. Tal vez me perdí algo? ¡Por favor comparte tu experiencia!
NOTA: Aquí hay otras situaciones en las que podemos tener necesidades de prueba similares.
Cualquier característica que funcione con algún tipo de tiempo de espera (por ejemplo, ¿juego de ajedrez?)
procesando una cola que desencadena eventos en un momento dado (en el escenario anterior podríamos seguir así: un día antes de que comience la reserva, quiero enviar un correo al usuario con todos los detalles. Escenario de prueba ...)
Desea configurar un entorno de prueba con datos recopilados en el pasado, le gustaría verlo como si fuera ahora. [1]
1 /programming/3271735/simulate-different-server-datetimes-in-php
2 /programming/4221480/how-to-change-current-time-for-unit-testing-date-functions-in-php
3 http://www.schmengler-se.de/en/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/
fuente
DateTime now
al código en lugar de un reloj.Respuestas:
Voy a usar un pseudocódigo similar a Python basado en sus casos de prueba para (con suerte) ayudar a explicar un poco esta respuesta, en lugar de solo exponer. En resumen: esta respuesta es básicamente un ejemplo de inyección de dependencia / inversión de dependencia de nivel de función única pequeña , en lugar de aplicar el principio a una clase / objeto completo.
En este momento, parece que sus pruebas están estructuradas de la siguiente manera:
Con implementaciones algo similar a:
En su lugar, intente olvidarse de "ahora" y diga a ambas funciones qué hora usar. Si la mayoría de las veces va a ser "ahora", puede hacer una de dos cosas principales para mantener el código de llamada simple:
None
(null
en PHP) y establecerlo en "ahora" si no se dio ningún valor.Por ejemplo, las dos funciones anteriores reescritas en la segunda versión podrían verse así:
De esta manera, las partes existentes del código no necesitan modificarse para pasar "ahora", mientras que las partes que realmente desea probar se pueden probar mucho más fácilmente:
También crea flexibilidad adicional en el futuro, por lo que es necesario cambiar menos si es necesario reutilizar las partes importantes. Para dos ejemplos que aparecen en la mente: una cola en la que las solicitudes deben crearse momentos demasiado tarde pero aún usan la hora correcta, o las confirmaciones tienen que suceder a las solicitudes pasadas como parte de las comprobaciones de integridad de datos.
Técnicamente, pensar de esta manera podría violar a YAGNI , pero en realidad no estamos construyendo para esos casos teóricos: este cambio sería solo para una prueba más fácil, que simplemente respalda esos casos teóricos.
Personalmente, trato de evitar cualquier tipo de burla tanto como sea posible, y prefiero las funciones puras como las anteriores. De esta forma, el código que se está probando es idéntico al código que se ejecuta fuera de las pruebas, en lugar de reemplazar las partes importantes con simulacros.
Hemos tenido una o dos regresiones que nadie notó durante un buen mes porque esa parte particular del código no se probó a menudo en el desarrollo (no estábamos trabajando activamente en él), se lanzó ligeramente roto (por lo que no hubo errores informes), y los simulacros utilizados estaban ocultando un error en la interacción entre las funciones de ayuda. Algunas modificaciones leves, por lo que no fueron necesarias simulacros y se pudieron probar muchas más, incluida la corrección de las pruebas en torno a esa regresión.
Este es el que debe evitarse a toda costa para las pruebas unitarias. Realmente no hay forma de saber qué tipo de inconsistencias o condiciones de carrera podrían introducirse para causar fallas intermitentes (para aplicaciones no relacionadas o compañeros de trabajo en el mismo cuadro de desarrollo), y una prueba fallida / errónea podría no retrasar el reloj correctamente.
fuente
Para mi dinero, mantener una dependencia en alguna clase de reloj es la forma más clara de manejar las pruebas dependientes del tiempo. En PHP, puede ser tan simple como una clase con una sola función
now
que devuelve la hora actual usandotime()
onew DateTime()
.Inyectar esta dependencia le permite manipular el tiempo en sus pruebas y usar el tiempo real en producción, sin tener que escribir demasiado código adicional.
fuente
Lo primero que debe hacer es eliminar los números mágicos de su código. En este caso, su número mágico de "5 minutos". No solo inevitablemente tendrá que cambiarlos más tarde cuando algún cliente lo solicite, sino que también mejorará las pruebas unitarias. ¿Quién va a esperar 5 minutos para que se ejecute su prueba?
En segundo lugar, si aún no lo ha hecho, use UTC para las marcas de tiempo reales. Esto evitará problemas con el horario de verano y la mayoría de los otros problemas de reloj.
En la prueba de la unidad, cambie la duración configurada a algo así como 500 ms y haga su prueba normal: ¿funciona de una manera antes del tiempo de espera y de otra manera después?
~ 1s todavía es bastante lento para las pruebas unitarias, y es probable que necesite algún tipo de umbral para tener en cuenta el rendimiento de la máquina y los cambios de reloj debido a NTP (u otra deriva). Pero en mi experiencia, esta es la mejor manera práctica de unir las cosas basadas en el tiempo de prueba en la mayoría de los sistemas. Simple, rápido, confiable. La mayoría de los otros enfoques (como ha encontrado) requieren toneladas de trabajo y / o son terriblemente propensos a errores.
fuente
Cuando aprendí TDD por primera vez, estaba trabajando en un entorno c # . La forma en que aprendí a hacerlo es tener un entorno de IoC completo, que también incluía un
ITimeService
, que era responsable de los servicios de todos los tiempos, y que fácilmente se podía burlar en las pruebas.Hoy en día estoy trabajando en rubí , y el tiempo de apriete es mucho más fácil: simplemente se desconecta :
fuente
Use Microsoft Fakes : en lugar de alterar su código para adaptarlo a la herramienta, la herramienta altera su código en tiempo de ejecución e inyecta un nuevo objeto Time (que usted define en su prueba) en lugar del reloj estándar.
Si está ejecutando un lenguaje dinámico, entonces Runkit es exactamente el mismo tipo de cosas.
Por ejemplo con falsificaciones:
Entonces, cuando el método de texto lo llame
return DateTime.Now
, devolverá el tiempo que inyectó, en lugar del tiempo actual real.fuente
Exploré la opción de espacios de nombres en PHP. Los espacios de nombres nos dan la oportunidad de agregar algunos comportamientos de prueba a la función DateTime. Por lo tanto, nuestros objetos originales permanecen intactos.
Primero, configuramos un objeto DateTime falso en nuestro espacio de nombres para que podamos, en nuestras pruebas, administrar a qué hora se considera "ahora".
Luego, en nuestras pruebas, podemos administrar este tiempo a través de las funciones estáticas DateTime :: set_now ($ my_time) y DateTime :: modify_now ("agregar algo de tiempo").
Primero viene un objeto de reserva falso . Este objeto es lo que queremos probar: observe el uso de la nueva fecha y hora () sin parámetros en dos lugares. Una breve descripción:
Nuestro objeto DateTime que anula el objeto DateTime principal tiene este aspecto . Todas las llamadas a funciones permanecen sin cambios. Solo "enganchamos" al constructor para simular nuestro comportamiento "ahora es cuando elijo". Más importante,
Nuestras pruebas son entonces muy sencillas. Va así con la unidad php (versión completa aquí ):
Esta estructura se puede reproducir fácilmente donde sea que necesite administrar "ahora" manualmente: solo necesito incluir el nuevo objeto DateTime y usar sus métodos estáticos en mis pruebas.
fuente
new Datetime
en un método separado para ser imitable.