Tempo beffardo nell'API java.time di Java 8


Risposte:


72

La cosa più vicina è l' Clockoggetto. È possibile creare un oggetto Orologio utilizzando l'ora che si desidera (o dall'ora corrente del sistema). Tutti gli oggetti date.time hanno nowmetodi di overload che accettano invece un oggetto clock per l'ora corrente. Quindi puoi usare l'inserimento delle dipendenze per iniettare un orologio con un orario specifico:

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

Vedi Clock JavaDoc per maggiori dettagli


11
Sì. In particolare, Clock.fixedè utile nei test, mentre Clock.systemo Clock.systemUTCpotrebbe essere utilizzato nell'applicazione.
Matt Johnson-Pint,

8
Peccato che non ci sia un orologio mutabile che mi permetta di impostarlo su un tempo senza ticchettio, ma modificare quell'ora in un secondo momento (puoi farlo con joda). Ciò sarebbe utile per testare codice sensibile al tempo, ad esempio una cache con scadenze basate sul tempo o una classe che pianifica eventi futuri.
bacar

2
@bacar L'orologio è una classe astratta, puoi creare la tua implementazione dell'orologio di prova
Bjarne Boström

Credo che sia quello che abbiamo finito per fare.
bacar

23

Ho usato una nuova classe per nascondere la Clock.fixedcreazione e semplificare i test:

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
Che dire della sicurezza dei thread se diversi test vengono eseguiti in parallelo e cambiano l'orologio di TimeMachine?
tuo

Devi passare l'orologio all'oggetto testato e usarlo quando chiami metodi relativi all'ora. E potresti rimuovere il getClock()metodo e utilizzare direttamente il campo. Questo metodo non aggiunge altro che poche righe di codice.
demone

1
Banktime o TimeMachine?
Emanuele

11

Ho usato un campo

private Clock clock;

e poi

LocalDate.now(clock);

nel mio codice di produzione. Quindi ho usato Mockito nei miei unit test per simulare l'orologio usando Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Beffardo:

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

Asserzione:

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

9

Trovo che l'uso di Clockclutter nel codice di produzione.

È possibile utilizzare JMockit o PowerMock per simulare invocazioni di metodi statici nel codice di test. Esempio 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);
}

EDIT : Dopo aver letto i commenti sulla risposta di Jon Skeet a una domanda simile qui su SO, non sono d'accordo con me stesso. Più di ogni altra cosa l'argomento mi ha convinto che non puoi mettere in parallelo i test quando prendi in giro metodi statici.

Tuttavia, puoi / devi ancora usare la derisione statica se devi gestire il codice legacy.


1
+1 per il commento "utilizzo di derisione statica per codice legacy". Quindi, per il nuovo codice, sii incoraggiato a fare affidamento su Dependency Injection e iniettare un Clock (Clock fisso per i test, Clock di sistema per il runtime di produzione).
David Groomes

1

Ho bisogno di LocalDateistanza invece di LocalDateTime.
Per questo motivo ho creato la seguente classe di utilità:

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);
    }
}

E l'utilizzo per esso è come:

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());
    }
}

Produzione:

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

Sostituita tutta la creazione LocalDate.now()in Clock.getCurrentDate()nel progetto.

Perché è un'applicazione di avvio primaverile . Prima testdell'esecuzione del profilo è sufficiente impostare una data predefinita per tutti i test:

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);
        }
    }
}

E aggiungi a spring.factories :

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


1

Ecco un modo funzionante per sovrascrivere l'ora del sistema corrente a una data specifica per scopi di test JUnit in un'applicazione Web Java 8 con EasyMock

Joda Time è sicuramente carino (grazie Stephen, Brian, avete reso il nostro mondo un posto migliore) ma non mi è stato permesso di usarlo.

Dopo alcuni esperimenti, alla fine ho escogitato un modo per simulare l'ora di una data specifica nell'API java.time di Java 8 con EasyMock

  • Senza Joda Time API
  • Senza PowerMock.

Ecco cosa è necessario fare:

Cosa deve essere fatto nella classe testata

Passo 1

Aggiungi un nuovo java.time.Clockattributo alla classe testata MyServicee assicurati che il nuovo attributo verrà inizializzato correttamente ai valori predefiniti con un blocco di istanza o un costruttore:

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

Passo 2

Iniettare il nuovo attributo clocknel metodo che richiede una data-ora corrente. Ad esempio, nel mio caso ho dovuto controllare se una data memorizzata nel database è avvenuta prima LocalDateTime.now(), che ho sostituito con LocalDateTime.now(clock), in questo modo:

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

Cosa deve essere fatto nella classe di prova

Passaggio 3

Nella classe di test, crea un oggetto mock clock e iniettalo nell'istanza della classe testata appena prima di chiamare il metodo testato doExecute(), quindi ripristinalo subito dopo, in questo modo:

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

Controllalo in modalità debug e vedrai che la data del 3 febbraio 2017 è stata correttamente inserita myServicenell'istanza e utilizzata nelle istruzioni di confronto, quindi è stata correttamente reimpostata alla data corrente con initDefaultClock().


0

Questo esempio mostra anche come combinare Instant e LocalTime ( spiegazione dettagliata dei problemi con la conversione )

Una classe in prova

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);
    }

}

Un test 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 l'aiuto di PowerMockito per un test di avvio primaverile puoi deridere il file ZonedDateTime. Hai bisogno di quanto segue.

Annotazioni

Nella classe di test è necessario preparare il servizio che utilizza l'estensione ZonedDateTime.

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

Scenario di prova

Nel test è possibile preparare un tempo desiderato e ottenerlo in risposta alla chiamata al metodo.

  @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 
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.