Tiempo de burla en la API java.time de Java 8


Respuestas:


72

Lo más cercano es el Clockobjeto. Puede crear un objeto Reloj utilizando la hora que desee (o desde la hora actual del Sistema). Todos los objetos date.time tienen nowmétodos sobrecargados que toman un objeto de reloj en lugar de la hora actual. Entonces puede usar la inyección de dependencia para inyectar un Reloj con una hora específica:

public class MyBean {
    private Clock clock;  // dependency inject
    ...
    public void process(LocalDate eventDate) {
      if (eventDate.isBefore(LocalDate.now(clock)) {
        ...
      }
    }
  }

Consulte Clock JavaDoc para obtener más detalles.


11
Si. En particular, Clock.fixedes útil en las pruebas, mientras que Clock.systemo Clock.systemUTCpodría usarse en la aplicación.
Matt Johnson-Pint

8
Es una pena que no haya un reloj mutable que me permita configurarlo en el tiempo sin tic-tac, pero modificar ese tiempo más tarde (puede hacer esto con joda). Esto sería útil para probar código sensible al tiempo, por ejemplo, un caché con vencimientos basados ​​en el tiempo o una clase que programa eventos en el futuro.
Bacar

2
@bacar Clock es una clase abstracta, puede crear su propia implementación de reloj de prueba
Bjarne Boström

Creo que eso es lo que terminamos haciendo.
bacar

23

Usé una nueva clase para ocultar la Clock.fixedcreación y simplificar las pruebas:

public class TimeMachine {

    private static Clock clock = Clock.systemDefaultZone();
    private static ZoneId zoneId = ZoneId.systemDefault();

    public static LocalDateTime now() {
        return LocalDateTime.now(getClock());
    }

    public static void useFixedClockAt(LocalDateTime date){
        clock = Clock.fixed(date.atZone(zoneId).toInstant(), zoneId);
    }

    public static void useSystemDefaultZoneClock(){
        clock = Clock.systemDefaultZone();
    }

    private static Clock getClock() {
        return clock ;
    }
}
public class MyClass {

    public void doSomethingWithTime() {
        LocalDateTime now = TimeMachine.now();
        ...
    }
}
@Test
public void test() {
    LocalDateTime twoWeeksAgo = LocalDateTime.now().minusWeeks(2);

    MyClass myClass = new MyClass();

    TimeMachine.useFixedClockAt(twoWeeksAgo);
    myClass.doSomethingWithTime();

    TimeMachine.useSystemDefaultZoneClock();
    myClass.doSomethingWithTime();

    ...
}

4
¿Qué pasa con la seguridad de los subprocesos si se ejecutan varias pruebas en paralelo y se cambia el reloj de TimeMachine?
youri

Debe pasar el reloj al objeto probado y usarlo cuando llame a métodos relacionados con el tiempo. Y puede eliminar el getClock()método y usar el campo directamente. Este método no agrega nada más que unas pocas líneas de código.
demonio

1
¿Banktime o TimeMachine?
Emanuele

11

Usé un campo

private Clock clock;

y entonces

LocalDate.now(clock);

en mi código de producción. Luego usé Mockito en mis pruebas unitarias para simular el Reloj usando Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Burlón:

fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
doReturn(fixedClock.instant()).when(clock).instant();
doReturn(fixedClock.getZone()).when(clock).getZone();

Afirmación:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));

9

Encuentro el uso de Clockdesorden en su código de producción.

Puede usar JMockit o PowerMock para simular invocaciones de métodos estáticos en su código de prueba. Ejemplo con JMockit:

@Test
public void testSth() {
  LocalDate today = LocalDate.of(2000, 6, 1);

  new Expectations(LocalDate.class) {{
      LocalDate.now(); result = today;
  }};

  Assert.assertEquals(LocalDate.now(), today);
}

EDITAR : Después de leer los comentarios sobre la respuesta de Jon Skeet a una pregunta similar aquí en SO , no estoy de acuerdo con mi yo pasado. Más que cualquier otra cosa, el argumento me convenció de que no se pueden paralizar las pruebas cuando se simulan métodos estáticos.

Sin embargo, puede / debe usar la simulación estática si tiene que lidiar con el código heredado.


1
+1 para el comentario de "usar simulacros estáticos para código heredado". Por lo tanto, para el código nuevo, anímese a apoyarse en la inyección de dependencia e inyectar un reloj (reloj fijo para pruebas, reloj del sistema para tiempo de ejecución de producción).
David Groomes

1

Necesito LocalDateinstancia en lugar de LocalDateTime.
Con tal razón creé la siguiente clase de utilidad:

public final class Clock {
    private static long time;

    private Clock() {
    }

    public static void setCurrentDate(LocalDate date) {
        Clock.time = date.toEpochDay();
    }

    public static LocalDate getCurrentDate() {
        return LocalDate.ofEpochDay(getDateMillis());
    }

    public static void resetDate() {
        Clock.time = 0;
    }

    private static long getDateMillis() {
        return (time == 0 ? LocalDate.now().toEpochDay() : time);
    }
}

Y su uso es como:

class ClockDemo {
    public static void main(String[] args) {
        System.out.println(Clock.getCurrentDate());

        Clock.setCurrentDate(LocalDate.of(1998, 12, 12));
        System.out.println(Clock.getCurrentDate());

        Clock.resetDate();
        System.out.println(Clock.getCurrentDate());
    }
}

Salida:

2019-01-03
1998-12-12
2019-01-03

Reemplazado toda la creación LocalDate.now()que Clock.getCurrentDate()en el proyecto.

Porque es una aplicación de arranque de primavera . Antes de la testejecución del perfil, simplemente establezca una fecha predefinida para todas las pruebas:

public class TestProfileConfigurer implements ApplicationListener<ApplicationPreparedEvent> {
    private static final LocalDate TEST_DATE_MOCK = LocalDate.of(...);

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
        if (environment.acceptsProfiles(Profiles.of("test"))) {
            Clock.setCurrentDate(TEST_DATE_MOCK);
        }
    }
}

Y agregue a spring.factories :

org.springframework.context.ApplicationListener = com.init.TestProfileConfigurer


1

Esta es una forma funcional de anular la hora actual del sistema a una fecha específica para propósitos de prueba JUnit en una aplicación web Java 8 con EasyMock

Joda Time es realmente agradable (gracias Stephen, Brian, han hecho de nuestro mundo un lugar mejor) pero no se me permitió usarlo.

Después de experimentar un poco, finalmente se me ocurrió una forma de simular el tiempo para una fecha específica en la API java.time de Java 8 con EasyMock

  • Sin la API de Joda Time
  • Sin PowerMock.

Esto es lo que debe hacerse:

Qué se debe hacer en la clase probada

Paso 1

Agregue un nuevo java.time.Clockatributo a la clase probada MyServicey asegúrese de que el nuevo atributo se inicialice correctamente en los valores predeterminados con un bloque de instanciación 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 atributo clocken 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ó antes LocalDateTime.now(), que reemplacé 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();
    }
  }
  // (...) 
}

Qué se debe 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, así:

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;  // Be this a specific 
  private int month = 2;    // date we need 
  private int day = 3;      // to simulate.

  @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(..)
    }
  }

Compruébelo en modo de depuración y verá que la fecha del 3 de febrero de 2017 se ha inyectado correctamente en la myServiceinstancia y se ha utilizado en la instrucción de comparación, y luego se ha restablecido correctamente a la fecha actual con initDefaultClock().


0

Este ejemplo incluso muestra cómo combinar Instant y LocalTime ( explicación detallada de los problemas con la conversión )

Una clase bajo prueba

import java.time.Clock;
import java.time.LocalTime;

public class TimeMachine {

    private LocalTime from = LocalTime.MIDNIGHT;

    private LocalTime until = LocalTime.of(6, 0);

    private Clock clock = Clock.systemDefaultZone();

    public boolean isInInterval() {

        LocalTime now = LocalTime.now(clock);

        return now.isAfter(from) && now.isBefore(until);
    }

}

Una prueba de Groovy

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

import java.time.Clock
import java.time.Instant

import static java.time.ZoneOffset.UTC
import static org.junit.runners.Parameterized.Parameters

@RunWith(Parameterized)
class TimeMachineTest {

    @Parameters(name = "{0} - {2}")
    static data() {
        [
            ["01:22:00", true,  "in interval"],
            ["23:59:59", false, "before"],
            ["06:01:00", false, "after"],
        ]*.toArray()
    }

    String time
    boolean expected

    TimeMachineTest(String time, boolean expected, String testName) {
        this.time = time
        this.expected = expected
    }

    @Test
    void test() {
        TimeMachine timeMachine = new TimeMachine()
        timeMachine.clock = Clock.fixed(Instant.parse("2010-01-01T${time}Z"), UTC)
        def result = timeMachine.isInInterval()
        assert result == expected
    }

}

0

Con la ayuda de PowerMockito para una prueba de arranque de primavera, puede burlarse del archivo ZonedDateTime. Necesita lo siguiente.

Anotaciones

En la clase de prueba, debe preparar el servicio que utiliza el ZonedDateTime.

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({EscalationService.class})
@SpringBootTest
public class TestEscalationCases {
  @Autowired
  private EscalationService escalationService;
  //...
}

Caso de prueba

En la prueba, puede preparar un tiempo deseado y obtenerlo en respuesta a la llamada al método.

  @Test
  public void escalateOnMondayAt14() throws Exception {
    ZonedDateTime preparedTime = ZonedDateTime.now();
    preparedTime = preparedTime.with(DayOfWeek.MONDAY);
    preparedTime = preparedTime.withHour(14);
    PowerMockito.mockStatic(ZonedDateTime.class);
    PowerMockito.when(ZonedDateTime.now(ArgumentMatchers.any(ZoneId.class))).thenReturn(preparedTime);
    // ... Assertions 
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.