Mockowanie czasu w java.time API Java 8

Odpowiedzi:

72

Najbliższą rzeczą jest Clockprzedmiot. Możesz utworzyć obiekt Clock w dowolnym momencie (lub z bieżącego czasu Systemu). Wszystkie obiekty date.time mają przeciążone nowmetody, które pobierają obiekt zegara zamiast aktualnego czasu. Więc możesz użyć iniekcji zależności, aby wstrzyknąć zegar z określonym czasem:

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

Aby uzyskać więcej informacji, zobacz Clock JavaDoc

dkatzel
źródło
11
Tak. W szczególności Clock.fixedjest przydatny w testowaniu, podczas gdy Clock.systemlub Clock.systemUTCmoże być używany w aplikacji.
Matt Johnson-Pint
8
Szkoda, że ​​nie ma zmiennego zegara, który pozwala mi ustawić czas bez tykania, ale zmodyfikuj ten czas później (możesz to zrobić za pomocą jody). Byłoby to przydatne do testowania kodu wrażliwego na czas, np. Pamięci podręcznej z wygasaniem zależnym od czasu lub klasy, która planuje zdarzenia w przyszłości.
Bacar
2
@bacar Clock to klasa abstrakcyjna, możesz stworzyć własną implementację testowego zegara
Bjarne Boström
Myślę, że to właśnie zrobiliśmy.
Bacar
23

Użyłem nowej klasy, aby ukryć Clock.fixedtworzenie i uprościć testy:

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
źródło
4
A co z bezpieczeństwem wątków, jeśli kilka testów jest wykonywanych równolegle i zmienia się zegar TimeMachine?
youri
Musisz przekazać zegar do testowanego obiektu i używać go przy wywoływaniu metod związanych z czasem. Możesz też usunąć getClock()metodę i bezpośrednio użyć pola. Ta metoda dodaje tylko kilka wierszy kodu.
deamon
1
Banktime czy TimeMachine?
Emanuele
11

Użyłem pola

private Clock clock;

i wtedy

LocalDate.now(clock);

w moim kodzie produkcyjnym. Następnie użyłem Mockito w moich testach jednostkowych, aby mockować Zegar za pomocą Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Kpiny:

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

Twierdzenie:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));
Claas Wilke
źródło
9

Uważam, że Clockzaśmiecam twój kod produkcyjny.

Można użyć JMockit lub PowerMock mock statycznej metody wywołania w kodzie testowym. Przykład z 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);
}

EDYCJA : Po przeczytaniu komentarzy do odpowiedzi Jona Skeeta na podobne pytanie tutaj na SO, nie zgadzam się z moim przeszłym sobą. Bardziej niż cokolwiek innego argument ten przekonał mnie, że nie można paralizować testów, gdy kpisz z metod statycznych.

Jednak możesz / musisz nadal używać statycznego mockowania, jeśli masz do czynienia ze starszym kodem.

Stefan Haberl
źródło
1
+1 za komentarz „używanie statycznego mockowania dla starszego kodu”. Tak więc w przypadku nowego kodu zachęcamy do skorzystania z Dependency Injection i wstrzyknięcia Zegara (stały zegar dla testów, zegar systemowy dla środowiska produkcyjnego).
David Groomes
1

Potrzebuję LocalDateinstancji zamiast LocalDateTime.
Z tego powodu stworzyłem następującą klasę użytkową:

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

A użycie do tego jest następujące:

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

Wynik:

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

Zastąpione całe stworzenie LocalDate.now(), aby Clock.getCurrentDate()w projekcie.

Ponieważ jest to aplikacja do rozruchu sprężynowego . Przed testwykonaniem profilu wystarczy ustawić predefiniowaną datę dla wszystkich testów:

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

I dodaj do wiosny. Fabryki :

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

catch23
źródło
1

Oto działający sposób na zastąpienie bieżącego czasu systemowego do określonej daty do celów testowania JUnit w aplikacji internetowej Java 8 za pomocą EasyMock

Joda Time jest z pewnością fajny (dziękuję Stephen, Brian, uczyniłeś nasz świat lepszym miejscem), ale nie pozwolono mi go używać.

Po kilku eksperymentach w końcu wymyśliłem sposób na udawanie czasu do określonej daty w java.time API Java 8 za pomocą EasyMock

  • Bez interfejsu API Joda Time
  • Bez PowerMock.

Oto, co należy zrobić:

Co należy zrobić w testowanej klasie

Krok 1

Dodaj nowy java.time.Clockatrybut do testowanej klasy MyServicei upewnij się, że nowy atrybut zostanie poprawnie zainicjowany z wartościami domyślnymi za pomocą bloku instancji lub konstruktora:

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

Krok 2

Wprowadź nowy atrybut clockdo metody, która wywołuje bieżącą datę-czas. Na przykład w moim przypadku musiałem sprawdzić, czy data zapisana w bazie danych wydarzyła się wcześniej LocalDateTime.now(), którą zamieniłem na LocalDateTime.now(clock):

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

Co należy zrobić w klasie testowej

Krok 3

W klasie testowej utwórz pozorowany obiekt zegara i wstrzyknij go do instancji testowanej klasy tuż przed wywołaniem testowanej metody doExecute(), a następnie zresetuj ją z powrotem, na przykład:

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

Sprawdź to w trybie debugowania, a zobaczysz, że data 3 lutego 2017 została poprawnie wstrzyknięta do myServiceinstancji i użyta w instrukcji porównawczej, a następnie została poprawnie zresetowana do aktualnej daty z initDefaultClock().

KiriSakow
źródło
0

Ten przykład pokazuje nawet, jak połączyć Instant i LocalTime ( szczegółowe wyjaśnienie problemów z konwersją )

Testowana klasa

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

}

Test Groovy'ego

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
    }

}
banterCZ
źródło
0

Z pomocą PowerMockito do testu sprężynowego rozruchu możesz kpić z ZonedDateTime. Potrzebujesz następujących elementów.

Adnotacje

Na zajęciach testowych musisz przygotować usługę korzystającą z ZonedDateTime.

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

Przypadek testowy

W teście możesz przygotować żądany czas i uzyskać go w odpowiedzi na wywołanie metody.

  @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
źródło