Anular Java System.currentTimeMillis para probar código sensible al tiempo

¿Hay alguna manera, ya sea en el código o con los argumentos de JVM, para anular la hora actual, tal como se presenta a través de 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 varios trabajos de contabilidad que giran gran parte de su lógica en torno a la fecha actual (es decir, el 1 ° del mes, el 1 ° del año, etc.)

Desafortunadamente, muchas de las funciones de llamadas heredadas de códigos, como new Date() o Calendar.getInstance() , que eventualmente llaman a System.currentTimeMillis .

Para fines de prueba, en este momento, estamos obligados a actualizar manualmente el reloj del sistema para manipular la hora y la fecha en que el código cree que se está ejecutando la prueba.

Entonces mi pregunta es:

¿Hay alguna manera de anular lo devuelto por System.currentTimeMillis ? Por ejemplo, ¿decirle a la JVM que agregue o reste algo de compensación automáticamente antes de regresar de ese método?

¡Gracias por adelantado!

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. Lo ideal sería hacerlo con la dependency injection, pero incluso si utilizara un singleton reemplazable, obtendría la capacidad de prueba.

Esto casi podría automatizarse con búsqueda y reemplazo para la versión singleton:

  • Reemplace Calendar.getInstance() con Clock.getInstance().getCalendarInstance() .
  • Reemplace la new Date() con Clock.getInstance().newDate()
  • Reemplace System.currentTimeMillis() con Clock.getInstance().currentTimeMillis()

(etc. según se requiera)

Una vez que haya dado ese primer paso, puede reemplazar el singleton con DI un poco a la vez.

tl; dr

¿Hay alguna manera, ya sea en el código o con los argumentos de JVM, para anular la hora actual, tal como se presenta a través de System.currentTimeMillis, que no sea cambiar manualmente el reloj del sistema en la máquina host?

Sí.

 Instant.now( Clock.fixed( Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC ) ) 

Clock en java.time

Tenemos una nueva solución al problema de un reemplazo de reloj enchufable para facilitar las pruebas 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:

para permitir el encendido de relojes alternativos cuando sea necesario

Puede conectar su propia implementación de Clock , aunque es probable que pueda encontrar uno ya hecho para satisfacer sus necesidades. Para su comodidad, java.time incluye métodos estáticos para generar implementaciones especiales. Estas implementaciones alternativas pueden ser valiosas durante la prueba.

Cadencia alterada

Los diversos métodos tick… producen relojes que incrementan el momento actual con una cadencia diferente.

El Clock predeterminado informa un tiempo actualizado con una frecuencia de milisegundos en Java 8 y en Java 9 tan fino como nanosegundos (dependiendo de su hardware). Puede solicitar que se informe el momento actual verdadero con una granularidad diferente.

  • tickSeconds – Incrementos en segundos enteros
  • tickMinutes – Incrementos en minutos completos
  • tick – Incrementos por el argumento de Duration aprobada.

Relojes falsos

Algunos relojes pueden mentir, produciendo un resultado diferente al del reloj del hardware del SO host.

  • fixed : informa un único momento inmutable (no incrementado) como el momento actual.
  • offset : informa el momento actual pero se desplaza por el argumento Duration transcurrido.

Por ejemplo, asegúrate en el primer momento de la Navidad más temprana de este año. en otras palabras, cuando Santa Claus y sus renos hacen su primera parada . La zona horaria más antigua actualmente parece ser Pacific/Kiritimati a +14:00 .

 LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) ); LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() ); ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ; ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone ); Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant(); Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone ); 

Usa 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, 10 AM en la fecha anterior del 24 de diciembre.

 Instant instant = Instant.now( clockEarliestXmasThisYear ); ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear ); 

instant.toString (): 2016-12-24T10: 00: 00Z

zdt.toString (): 2016-12-25T00: 00 + 14: 00 [Pacific / Kiritimati]

Vea el código en vivo en IdeOne.com .

Hora verdadera, zona horaria diferente

Puede controlar qué zona horaria es asignada por la implementación del Clock . 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 ZoneId opcionales ZoneId o ZoneOffset .

Puede especificar que UTC sea la zona predeterminada.

 ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () ); 

Puede especificar cualquier zona horaria en particular. Especifique un nombre de zona horaria adecuada en el formato continent/region , como America/Montreal , Africa/Casablanca o Pacific/Auckland . Nunca utilice la abreviatura de 3-4 letras, como EST o IST ya que no son zonas horarias verdaderas, no están estandarizadas y ni siquiera son únicas (!).

 ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) ); 

Puede especificar que la zona horaria predeterminada actual de la JVM sea la predeterminada para un objeto Clock particular.

 ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () ); 

Ejecute este código para comparar. Tenga en cuenta que todos ellos informan el mismo momento, el mismo punto en la línea de tiempo. Difieren solo en el tiempo del reloj de pared ; en otras palabras, tres formas de decir lo mismo, tres formas de mostrar el mismo momento.

 System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC ); System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem ); System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone ); 

America/Los_Angeles fue la zona predeterminada actual de JVM en la computadora que ejecutó este código.

zdtClockSystemUTC.toString (): 2016-12-31T20: 52: 39.688Z

zdtClockSystem.toString (): 2016-12-31T15: 52: 39.750-05: 00 [America / Montreal]

zdtClockSystemDefaultZone.toString (): 2016-12-31T12: 52: 39.762-08: 00 [America / Los_Angeles]

La clase Instant siempre está en UTC por definición. Entonces, estos tres usos del Clock relacionados con la zona tienen exactamente el mismo efecto.

 Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () ); Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) ); Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () ); 

instantClockSystemUTC.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystem.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystemDefaultZone.toString (): 2016-12-31T20: 52: 39.763Z

Reloj predeterminado

La implementación utilizada por defecto para Instant.now es la que devuelve Clock.systemUTC() . Esta es la implementación utilizada cuando no especifica un Clock . Instant.now usted mismo en el código fuente Java 9 de Instant.now para Instant.now .

 public static Instant now() { return Clock.systemUTC().instant(); } 

El Clock predeterminado para OffsetDateTime.now y ZonedDateTime.now es Clock.systemDefaultZone() . Ver el código fuente

 public static ZonedDateTime now() { return now(Clock.systemDefaultZone()); } 

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 su computadora.


Acerca de java.time

El marco java.time está integrado en Java 8 y posterior. Estas clases suplantan a las problemáticas antiguas clases heredadas de fecha y hora como java.util.Date , Calendar y SimpleDateFormat .

El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a las clases java.time .

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 .

Puede intercambiar objetos java.time directamente con su base de datos. Use un controlador JDBC que cumpla con JDBC 4.2 o posterior. Sin necesidad de cadenas, sin necesidad de clases java.sql.* .

¿Dónde obtener las clases de java.time?

  • Java SE 8 , Java SE 9 y posterior
    • Incorporado.
    • Parte de la API Java estándar con una implementación integrada.
    • Java 9 agrega algunas características y correcciones menores.
  • Java SE 6 y Java SE 7
    • Gran parte de la funcionalidad de java.time se transfiere a Java 6 y 7 en ThreeTen-Backport .
  • Androide
    • Versiones posteriores de las implementaciones del paquete de Android de las clases java.time.
    • Para Android anterior (<26), el proyecto ThreeTenABP adapta ThreeTen-Backport (mencionado anteriormente). Consulte Cómo utilizar ThreeTenABP ….

El proyecto ThreeTen-Extra amplía java.time con clases adicionales. Este proyecto es un terreno de prueba para posibles adiciones futuras a java.time. Puede encontrar algunas clases útiles aquí, como Interval , YearWeek , YearQuarter y más .

Como dijo Jon Skeet :

“usar Joda Time” es casi siempre la mejor respuesta a cualquier pregunta que involucre “¿cómo logro X con java.util.Date/Calendar?”

Así que aquí va (suponiendo que acaba de reemplazar toda su new Date() con el new DateTime().toDate() )

 //Change to specific time DateTimeUtils.setCurrentMillisFixed(millis); //or set the clock to be a difference from system time DateTimeUtils.setCurrentMillisOffset(millis); //Reset to system time DateTimeUtils.setCurrentMillisSystem(); 

Si desea importar una biblioteca que tenga una interfaz (consulte el comentario de Jon a continuación), puede usar Prevayler’s Clock , que proporcionará implementaciones además de la interfaz estándar. El contenedor completo solo tiene 96kB, por lo que no debería romper el banco …

Si bien el uso de 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 la implementación que depende de System.currentTimeMillis (las hay).

Es por eso que usamos jmockit para burlar el tiempo del sistema directamente:

 import mockit.Mock; import mockit.MockClass; ... @MockClass(realClass = System.class) public static class SystemMock { /** * Fake current time millis returns value modified by required offset. * * @return fake "current" millis */ @Mock public static long currentTimeMillis() { return INIT_MILLIS + offset + millisSinceClassInit(); } } Mockit.setUpMock(SystemMock.class); 

Debido a que no es posible llegar al valor original de millis no validado, usamos nano timer en su lugar, esto no está relacionado con el reloj de pared, pero el tiempo relativo es suficiente aquí:

 // runs before the mock is applied private static final long INIT_MILLIS = System.currentTimeMillis(); private static final long INIT_NANOS = System.nanoTime(); private static long millisSinceClassInit() { return (System.nanoTime() - INIT_NANOS) / 1000000; } 

Existe un problema documentado, que con HotSpot el tiempo vuelve a la normalidad después de una serie de llamadas: aquí está el informe de problemas: http://code.google.com/p/jmockit/issues/detail?id=43

Para superar esto, debemos activar una optimización de 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 presenta solo debido a las pruebas. Sería bueno tener la opción de JVM incorporada para ejecutar en diferentes momentos, lástima que no es posible sin hacks como este.

La historia completa está en mi publicación del blog aquí: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

Completa clase útil SystemTimeShifter se proporciona en la publicación. La clase se puede utilizar en sus pruebas, o se puede utilizar como la primera clase principal antes de su clase principal real con mucha facilidad para ejecutar su aplicación (o incluso todo el servidor de aplicaciones) en un momento diferente. Por supuesto, esto se intenta con fines de prueba principalmente, no para el entorno de producción.

EDITAR Julio de 2014: JMockit ha cambiado mucho últimamente y está obligado a usar JMockit 1.0 para usarlo correctamente (IIRC). Definitivamente no se puede actualizar a la versión más nueva 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.

Powermock funciona muy bien. Solo lo usé para simular System.currentTimeMillis() .

Utilice la Progtwigció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 entreteje las clases de aplicación para redirigir la llamada a System.currentTimeMillis() o a la new Date() a otra clase de utilidad propia.

Sin embargo, las clases de sistema de tejido ( java.lang.* ) Son un poco más complicadas y es posible que deba realizar un tejido sin conexión para rt.jar y utilizar un JDK / rt.jar por separado para sus pruebas.

Se llama Tejido Binario y también hay herramientas especiales para realizar el tejido de las clases del Sistema y eludir algunos problemas con eso (por ejemplo, el arranque de la máquina virtual puede no funcionar)

Realmente no hay una manera de hacer esto directamente en la máquina virtual, pero podría haber algo para configurar programáticamente la hora del sistema en la máquina de prueba. La mayoría (todos?) Del sistema operativo tienen comandos de línea de comando para hacer esto.

Sin hacer re-factoring, ¿puede ejecutar la prueba en una instancia de máquina virtual del sistema operativo?

En mi opinión, solo una solución no invasiva puede funcionar. Especialmente si tiene librerías externas y una gran base de códigos heredados, no hay una manera confiable de burlarse del tiempo.

JMockit … funciona solo por un número limitado de veces

PowerMock & Co … necesita simular los clientes a System.currentTimeMillis (). Nuevamente 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 señalar esa solución?

@jarnbjo: ¿podría mostrar algo del código javaagent por favor?

Una forma efectiva de anular el tiempo actual del sistema para probar JUnit en una aplicación web Java 8 con EasyMock, sin Joda Time y sin PowerMock.

Esto es lo que necesita hacer:

Lo que debe hacerse en la clase probada

Paso 1

Agregue un nuevo atributo java.time.Clock a la clase probada MyService y asegúrese de que el nuevo atributo se inicialice correctamente en los valores predeterminados con un bloque de creación de instancias o un constructor:

 import java.time.Clock; import java.time.LocalDateTime; public class MyService { // (...) private Clock clock; public Clock getClock() { return clock; } public void setClock(Clock newClock) { clock = newClock; } public void initDefaultClock() { setClock( Clock.system( Clock.systemDefaultZone().getZone() // You can just as well use // java.util.TimeZone.getDefault().toZoneId() instead ) ); } { initDefaultClock(); // initialisation in an instantiation block, but // it can be done in a constructor just as well } // (...) } 

Paso 2

Inyecte el nuevo clock atributos en el método que requiere una fecha-hora actual. Por ejemplo, en mi caso tuve que verificar si una fecha almacenada en dataase sucedió antes de LocalDateTime.now() , que LocalDateTime.now(clock) con LocalDateTime.now(clock) , así:

 import java.time.Clock; import java.time.LocalDateTime; public class MyService { // (...) protected void doExecute() { LocalDateTime dateToBeCompared = someLogic.whichReturns().aDate().fromDB(); while (dateToBeCompared.isBefore(LocalDateTime.now(clock))) { someOtherLogic(); } } // (...) } 

Lo que debe hacerse 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 forma:

 import java.time.Clock; import java.time.LocalDateTime; import java.time.OffsetDateTime; import org.junit.Test; public class MyServiceTest { // (...) private int year = 2017; private int month = 2; private int day = 3; @Test public void doExecuteTest() throws Exception { // (...) EasyMock stuff like mock(..), expect(..), replay(..) and whatnot MyService myService = new MyService(); Clock mockClock = Clock.fixed( LocalDateTime.of(year, month, day, 0, 0).toInstant(OffsetDateTime.now().getOffset()), Clock.systemDefaultZone().getZone() // or java.util.TimeZone.getDefault().toZoneId() ); myService.setClock(mockClock); // set it before calling the tested method myService.doExecute(); // calling tested method myService.initDefaultClock(); // reset the clock to default right afterwards with our own previously created method // (...) remaining EasyMock stuff: verify(..) and assertEquals(..) } } 

Verifíquelo en modo de depuración y verá que la fecha del 3 de febrero de 2017 se ha inyectado correctamente en la instancia myService y se ha utilizado en las instrucciones de comparación, y luego se ha restablecido correctamente a la fecha actual con initDefaultClock() .

Si está ejecutando Linux, puede usar la twig principal de libfaketime o, en el momento de la prueba, confirmar 4ce2835 .

Simplemente configure la variable de entorno con la hora en la que le gustaría burlarse de su aplicación java y ejecútela utilizando ld-preloading:

 # bash export FAKETIME="1985-10-26 01:21:00" export DONT_FAKE_MONOTONIC=1 LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar 

La segunda variable de entorno es primordial para las aplicaciones de Java, que de lo contrario se congelarían. Requiere la twig principal de libfaketime en el momento de la escritura.

Si desea cambiar la hora de un servicio administrado por sistema, solo 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 :

 [Service] Environment="FAKETIME=2017-10-31 23:00:00" Environment="DONT_FAKE_MONOTONIC=1" Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1" 

No te olvides de volver a cargar systemd usando `systemctl daemon-reload

Si desea simular el método con el argumento System.currentTimeMillis() , puede pasar anyLong() 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, estoy usando frameworks de PowerMock y Mockito.