Mocking time dans l'API java.time de Java 8

Réponses:

72

La chose la plus proche est l' Clockobjet. Vous pouvez créer un objet Horloge à tout moment (ou à partir de l'heure actuelle du système). Tous les objets date.time ont des nowméthodes surchargées qui prennent un objet horloge à la place pour l'heure actuelle. Vous pouvez donc utiliser l'injection de dépendances pour injecter une horloge avec une heure spécifique:

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

Voir Clock JavaDoc pour plus de détails

Dkatzel
la source
11
Oui. En particulier, Clock.fixedest utile pour les tests, pendant Clock.systemou Clock.systemUTCpourrait être utilisé dans l'application.
Matt Johnson-Pint
8
Dommage qu'il n'y ait pas d'horloge mutable qui me permette de la régler sur une heure non cochée, mais modifiez cette heure plus tard (vous pouvez le faire avec joda). Cela serait utile pour tester du code sensible au temps, par exemple un cache avec des expirations basées sur le temps ou une classe qui planifie des événements dans le futur.
bacar
2
@bacar Clock est une classe abstraite, vous pouvez créer votre propre implémentation d'horloge de test
Bjarne Boström
Je crois que c'est ce que nous avons fini par faire.
bacar
23

J'ai utilisé une nouvelle classe pour masquer la Clock.fixedcréation et simplifier les tests:

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

    ...
}
LuizSignorelli
la source
4
Qu'en est-il de la sécurité des threads si plusieurs tests sont exécutés en parallèle et modifient l'horloge TimeMachine?
youri
Vous devez transmettre l'horloge à l'objet testé et l'utiliser lors de l'appel de méthodes liées au temps. Et vous pouvez supprimer la getClock()méthode et utiliser le champ directement. Cette méthode n'ajoute rien d'autre que quelques lignes de code.
démon
1
Banktime ou TimeMachine?
Emanuele
11

J'ai utilisé un champ

private Clock clock;

puis

LocalDate.now(clock);

dans mon code de production. Ensuite, j'ai utilisé Mockito dans mes tests unitaires pour simuler l'horloge en utilisant Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Railleur:

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

Affirmation:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));
Claas Wilke
la source
9

Je trouve que l'utilisation Clockencombre votre code de production.

Vous pouvez utiliser JMockit ou PowerMock pour simuler les appels de méthode statique dans votre code de test. Exemple avec 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 : Après avoir lu les commentaires sur la réponse de Jon Skeet à une question similaire ici sur SO, je ne suis pas d'accord avec mon passé. Plus que toute autre chose, l'argument m'a convaincu que vous ne pouvez pas paralléliser les tests lorsque vous vous moquez de méthodes statiques.

Cependant, vous pouvez / devez toujours utiliser la simulation statique si vous devez gérer du code hérité.

Stefan Haberl
la source
1
+1 pour le commentaire "Utilisation de la simulation statique pour l'ancien code". Donc, pour un nouveau code, soyez encouragé à vous pencher sur l'injection de dépendances et à injecter une horloge (horloge fixe pour les tests, horloge système pour l'exécution de la production).
David Groomes
1

J'ai besoin d'une LocalDateinstance au lieu de LocalDateTime.
Pour cette raison, j'ai créé la classe utilitaire suivante:

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

Et son utilisation est comme:

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

Production:

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

Remplacée toute la création LocalDate.now()à Clock.getCurrentDate()dans le projet.

Parce que c'est une application de démarrage à ressort . Avant l' testexécution du profil, définissez simplement une date prédéfinie pour tous les tests:

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

Et ajoutez à spring.factories :

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

attraper23
la source
1

Voici une méthode de travail pour remplacer l'heure système actuelle par une date spécifique à des fins de test JUnit dans une application Web Java 8 avec EasyMock

Joda Time est bien sûr gentil (merci Stephen, Brian, vous avez rendu notre monde meilleur) mais je n'ai pas été autorisé à l'utiliser.

Après quelques expérimentations, j'ai finalement trouvé un moyen de simuler l'heure à une date spécifique dans l'API java.time de Java 8 avec EasyMock

  • Sans API Joda Time
  • Sans PowerMock.

Voici ce qu'il faut faire:

Que faut-il faire dans la classe testée

Étape 1

Ajoutez un nouvel java.time.Clockattribut à la classe testée MyServiceet assurez-vous que le nouvel attribut sera correctement initialisé aux valeurs par défaut avec un bloc d'instanciation ou un constructeur:

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

Étape 2

Injectez le nouvel attribut clockdans la méthode qui appelle une date-heure actuelle. Par exemple, dans mon cas, j'ai dû vérifier si une date stockée dans la base de données était antérieure LocalDateTime.now(), que j'ai remplacée par LocalDateTime.now(clock), comme ceci:

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

Que faut-il faire dans la classe de test

Étape 3

Dans la classe de test, créez un objet d'horloge fictif et injectez-le dans l'instance de la classe testée juste avant d'appeler la méthode testée doExecute(), puis réinitialisez-le juste après, comme ceci:

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

Vérifiez-le en mode débogage et vous verrez que la date du 3 février 2017 a été correctement injectée dans l' myServiceinstance et utilisée dans l'instruction de comparaison, puis a été correctement réinitialisée à la date actuelle avec initDefaultClock().

KiriSakow
la source
0

Cet exemple montre même comment combiner Instant et LocalTime ( explication détaillée des problèmes de conversion )

Une classe sous test

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
    }

}
plaisanterieCZ
la source
0

Avec l'aide de PowerMockito pour un test de démarrage à ressort, vous pouvez vous moquer du ZonedDateTime. Vous avez besoin des éléments suivants.

Annotations

Sur la classe de test, vous devez préparer le service qui utilise le ZonedDateTime.

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

Cas de test

Dans le test, vous pouvez préparer l'heure souhaitée et l'obtenir en réponse à l'appel de méthode.

  @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 
}
pasquale
la source