¿Hay alguna forma, ya sea en código o con argumentos JVM, para anular la hora actual, tal como se presenta a través System.currentTimeMillis
, que no sea cambiar manualmente el reloj del sistema en la máquina host?
Un poco de historia:
Tenemos un sistema que ejecuta una serie de trabajos de contabilidad que giran gran parte de su lógica en torno a la fecha actual (es decir, 1 de mes, 1 de año, etc.)
Desafortunadamente, muchos de los códigos heredados llaman a funciones tales como new Date()
o Calendar.getInstance()
, las cuales eventualmente llaman a System.currentTimeMillis
.
Para fines de prueba, en este momento, estamos atascados con la actualización manual del reloj del sistema para manipular a qué hora y fecha el código cree que la prueba se está ejecutando.
Entonces mi pregunta es:
¿Hay alguna forma de anular lo que devuelve System.currentTimeMillis
? Por ejemplo, ¿decirle a la JVM que agregue o reste automáticamente algún desplazamiento antes de regresar de ese método?
¡Gracias por adelantado!
fuente
Respuestas:
Recomiendo encarecidamente que, en lugar de jugar con el reloj del sistema, muerda la bala y refactorice ese código heredado para usar un reloj reemplazable. Idealmente, eso debería hacerse con la inyección de dependencia, pero incluso si usara un singleton reemplazable, obtendría capacidad de prueba.
Esto podría casi automatizarse con la búsqueda y reemplazo de la versión singleton:
Calendar.getInstance()
conClock.getInstance().getCalendarInstance()
.new Date()
conClock.getInstance().newDate()
System.currentTimeMillis()
conClock.getInstance().currentTimeMillis()
(etc. según sea necesario)
Una vez que haya dado el primer paso, puede reemplazar el singleton con DI un poco a la vez.
fuente
java.time.Clock
clase "para permitir la conexión de relojes alternativos cuando sea necesario".static
métodos, asegúrese de que no sean OO, pero inyectar una dependencia sin estado cuya instancia no opere en ningún estado de instancia no es realmente mejor; de todos modos, es solo una forma elegante de disfrazar lo que efectivamente es un comportamiento "estático".tl; dr
Si.
Clock
En java.timeTenemos una nueva solución al problema de un reemplazo de reloj conectable para facilitar la prueba con valores falsos de fecha y hora. El paquete java.time en Java 8 incluye una clase abstracta
java.time.Clock
, con un propósito explícito:Puede conectar su propia implementación de
Clock
, aunque es probable que pueda encontrar una ya hecha para satisfacer sus necesidades. Para su conveniencia, java.time incluye métodos estáticos para producir implementaciones especiales. Estas implementaciones alternativas pueden ser valiosas durante las pruebas.Cadencia alterada
Los diversos
tick…
métodos producen relojes que incrementan el momento actual con una cadencia diferente.El valor predeterminado
Clock
informa un tiempo actualizado con tanta frecuencia como milisegundos en Java 8 y en Java 9 tan fino como nanosegundos (dependiendo de su hardware). Puede solicitar que se informe el momento actual real con una granularidad diferente.tickSeconds
- Incrementos en segundos completostickMinutes
- Incrementos en minutos completostick
- Incrementos por elDuration
argumento pasado .Relojes falsos
Algunos relojes pueden mentir, produciendo un resultado diferente al del reloj de hardware del sistema operativo host.
fixed
- Informa un momento único que no cambia (sin incremento) como el momento actual.offset
- Informa el momento actual, pero cambiado por elDuration
argumento pasado .Por ejemplo, asegure el primer momento de la primera Navidad de este año. en otras palabras, cuando Santa y sus renos hacen su primera parada . La zona horaria más temprana hoy en día parece estar
Pacific/Kiritimati
en+14:00
.Use ese reloj fijo especial para regresar siempre el mismo momento. Tenemos el primer momento del día de Navidad en Kiritimati , con UTC mostrando un reloj de pared de catorce horas antes, las 10 de la mañana de la fecha anterior del 24 de diciembre.
Ver código en vivo en IdeOne.com .
Hora verdadera, zona horaria diferente
Puede controlar qué zona horaria asigna la
Clock
implementación. Esto podría ser útil en algunas pruebas. Pero no lo recomiendo en el código de producción, donde siempre debe especificar explícitamente los opcionalesZoneId
o losZoneOffset
argumentos.Puede especificar que UTC sea la zona predeterminada.
Puede especificar cualquier zona horaria en particular. Especificar un nombre de zona horaria correcta en el formato de
continent/region
, por ejemploAmerica/Montreal
,Africa/Casablanca
oPacific/Auckland
. Nunca use el 3-4 abreviatura de letras tales comoEST
oIST
, ya que son no verdaderas zonas de tiempo, no estandarizados, y ni siquiera único (!).Puede especificar que la zona horaria predeterminada actual de la JVM sea la predeterminada para un
Clock
objeto en particular .Ejecute este código para comparar. Tenga en cuenta que todos informan el mismo momento, el mismo punto en la línea de tiempo. Solo difieren en la hora del reloj de pared ; en otras palabras, tres formas de decir lo mismo, tres formas de mostrar el mismo momento.
America/Los_Angeles
era la zona predeterminada actual de JVM en la computadora que ejecutó este código.La
Instant
clase siempre está en UTC por definición. Entonces, estos tresClock
usos relacionados con la zona tienen exactamente el mismo efecto.Reloj predeterminado
La implementación utilizada por defecto para
Instant.now
es la que devuelveClock.systemUTC()
. Esta es la implementación utilizada cuando no especificas aClock
. Compruébelo usted mismo en el código fuente de Java 9 previo al lanzamiento deInstant.now
.El valor predeterminado
Clock
paraOffsetDateTime.now
yZonedDateTime.now
esClock.systemDefaultZone()
. Ver código fuente .El comportamiento de las implementaciones predeterminadas cambió entre Java 8 y Java 9. En Java 8, el momento actual se captura con una resolución solo en milisegundos a pesar de la capacidad de las clases para almacenar una resolución de nanosegundos . Java 9 trae una nueva implementación capaz de capturar el momento actual con una resolución de nanosegundos, dependiendo, por supuesto, de la capacidad del reloj de hardware de su computadora.
Sobre java.time
El marco java.time está integrado en Java 8 y versiones posteriores. Estas clases suplantar la vieja problemáticos heredados clases de fecha y hora como
java.util.Date
,Calendar
, ySimpleDateFormat
.Para obtener más información, consulte el Tutorial de Oracle . Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310 .
El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a las clases java.time .
Puede intercambiar objetos java.time directamente con su base de datos. Utilice un controlador JDBC compatible con JDBC 4.2 o posterior. No hay necesidad de cadenas, no hay necesidad de
java.sql.*
clases. Hibernate 5 y JPA 2.2 admiten java.time .¿Dónde obtener las clases java.time?
fuente
Como dijo Jon Skeet :
Así que aquí va (suponiendo que hayas reemplazado todo
new Date()
connew DateTime().toDate()
)Si desea importar una biblioteca que tenga una interfaz (vea el comentario de Jon a continuación), puede usar Prevayler's Clock , que proporcionará implementaciones y la interfaz estándar. El frasco lleno tiene solo 96kB, por lo que no debería romper el banco ...
fuente
Si bien usar algún patrón de DateFactory parece agradable, no cubre las bibliotecas que no puede controlar; imagine la anotación de validación @Past con una implementación que se basa en System.currentTimeMillis (existe).
Es por eso que usamos jmockit para burlarse del tiempo del sistema directamente:
Debido a que no es posible llegar al valor original no desbloqueado de millis, en su lugar utilizamos un temporizador nano; esto no está relacionado con el reloj de pared, pero el tiempo relativo es suficiente aquí:
Hay un problema documentado, que con HotSpot el tiempo vuelve a la normalidad después de varias llamadas: aquí está el informe del problema: http://code.google.com/p/jmockit/issues/detail?id=43
Para superar esto, tenemos que activar una optimización HotSpot específica: ejecutar JVM con este argumento
-XX:-Inline
.Si bien esto puede no ser perfecto para la producción, está bien para las pruebas y es absolutamente transparente para la aplicación, especialmente cuando DataFactory no tiene sentido comercial y se introduce solo por las pruebas. Sería bueno tener la opción de JVM incorporada para ejecutarse en un tiempo diferente, lástima que no sea posible sin hacks como este.
La historia completa está en mi blog aquí: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/
Completa clase práctica SystemTimeShifter se proporciona en la publicación. La clase se puede usar en sus pruebas, o se puede usar como la primera clase principal antes de su clase principal real muy fácilmente para ejecutar su aplicación (o incluso el servidor de aplicaciones completo) en un momento diferente. Por supuesto, esto está destinado principalmente a fines de prueba, no para entornos de producción.
EDITAR Julio de 2014: JMockit ha cambiado mucho últimamente y seguramente usará JMockit 1.0 para usarlo correctamente (IIRC). Definitivamente no puedo actualizar a la última versión donde la interfaz es completamente diferente. Estaba pensando en incluir solo las cosas necesarias, pero como no necesitamos esto en nuestros nuevos proyectos, no estoy desarrollando esto en absoluto.
fuente
Powermock funciona muy bien. Solo lo usé para burlarme
System.currentTimeMillis()
.fuente
@PrepareForTest
anotación en la clase de prueba).System.currentTimeMillis
cualquier parte de su classpath (cualquier lib), debe verificar cada clase. A eso me refería, nada más. El punto es que te burlas de ese comportamiento del lado de la persona que llama ("específicamente lo dices"). Esto está bien para pruebas simples, pero no para pruebas en las que no puede estar seguro de qué llama a ese método desde dónde (por ejemplo, pruebas de componentes más complejas con bibliotecas involucradas). Eso no significa que Powermock esté mal en absoluto, solo significa que no puede usarlo para este tipo de prueba.Utilice la Programación orientada a aspectos (AOP, por ejemplo, AspectJ) para tejer la clase del Sistema para devolver un valor predefinido que podría establecer dentro de sus casos de prueba.
O tejer las clases de la aplicación para redirigir la llamada a
System.currentTimeMillis()
onew Date()
a otra clase de utilidad de su propio.java.lang.*
Sin embargo, las clases del sistema de tejido ( ) son un poco más complicadas y es posible que deba realizar un tejido fuera de línea para rt.jar y usar un JDK / rt.jar separado para sus pruebas.Se llama tejido binario y también hay herramientas especiales para realizar el tejido de las clases del sistema y evitar algunos problemas con eso (por ejemplo, el arranque de la VM puede no funcionar)
fuente
Realmente no hay una manera de hacer esto directamente en la máquina virtual, pero podría hacer algo para programar mediante programación la hora del sistema en la máquina de prueba. La mayoría de los sistemas operativos (¿todos?) Tienen comandos de línea de comandos para hacer esto.
fuente
date
ytime
. En Linux eldate
comando.Una forma de trabajar para anular el tiempo actual del sistema para propósitos de prueba JUnit en una aplicación web Java 8 con EasyMock, sin Joda Time y sin PowerMock.
Esto es lo que debes hacer:
Lo que hay que hacer en la clase probada
Paso 1
Agregue un nuevo
java.time.Clock
atributo a la clase probadaMyService
y asegúrese de que el nuevo atributo se inicializará correctamente en los valores predeterminados con un bloque de instanciación o un constructor:Paso 2
Inyecte el nuevo atributo
clock
en el método que requiere una fecha y hora actual. Por ejemplo, en mi caso tuve que realizar una verificación de si una fecha almacenada en la base de datos sucedió antesLocalDateTime.now()
, con lo que remplacéLocalDateTime.now(clock)
, así:Lo que hay que hacer en la clase de prueba.
Paso 3
En la clase de prueba, cree un objeto de reloj simulado e inyéctelo en la instancia de la clase probada justo antes de llamar al método probado
doExecute()
, luego reinícielo inmediatamente después, de esta manera:Verifíquelo en modo de depuración y verá que la fecha del 3 de febrero de 2017 se ha inyectado correctamente en la
myService
instancia y se ha utilizado en las instrucciones de comparación, y luego se ha restablecido correctamente a la fecha actual coninitDefaultClock()
.fuente
En mi opinión, solo una solución no invasiva puede funcionar. Especialmente si tiene bibliotecas externas y una gran base de código heredado, no hay una forma confiable de burlarse del tiempo.
JMockit ... funciona solo por un número restringido de veces
PowerMock & Co ... necesita burlarse de los clientes a System.currentTimeMillis (). De nuevo una opción invasiva.
A partir de esto, solo veo que el enfoque javaagent o aop mencionado es transparente para todo el sistema. ¿Alguien ha hecho eso y podría apuntar a tal solución?
@jarnbjo: ¿podría mostrar algo del código de Java por favor?
fuente
Si está ejecutando Linux, puede usar la rama maestra de libfaketime, o al momento de probar commit 4ce2835 .
Simplemente configure la variable de entorno con el tiempo con el que desea simular su aplicación Java y ejecútela usando ld-preloading:
La segunda variable de entorno es primordial para las aplicaciones java, que de lo contrario se congelarían. Requiere la rama maestra de libfaketime en el momento de la escritura.
Si desea cambiar la hora de un servicio administrado systemd, simplemente agregue lo siguiente a las anulaciones de archivos de su unidad, por ejemplo, para elasticsearch esto sería
/etc/systemd/system/elasticsearch.service.d/override.conf
:No olvides recargar systemd usando `systemctl daemon-reload
fuente
Si desea burlarse del método que tiene
System.currentTimeMillis()
argumento, puede pasaranyLong()
de la clase Matchers como argumento.PD: Puedo ejecutar mi caso de prueba con éxito usando el truco anterior y solo para compartir más detalles sobre mi prueba de que estoy usando los marcos de PowerMock y Mockito.
fuente