Время фиксации в Java 8 java.time API


Ответы:


73

Самое близкое - это Clockобъект. Вы можете создать объект Clock, используя любое время, которое хотите (или текущее время системы). Все объекты date.time имеют перегруженные nowметоды, которые вместо текущего времени принимают объект часов. Таким образом, вы можете использовать инъекцию зависимостей, чтобы ввести часы с определенным временем:

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

См. Clock JavaDoc для более подробной информации.


11
Да. В частности, Clock.fixedполезно при тестировании, в то время Clock.systemили Clock.systemUTCмогут быть использованы в приложении.
Мэтт Джонсон-Пинт

8
Жалко, что нет изменяемых часов, которые позволяют мне установить время без отсчета времени, но изменить это время позже (вы можете сделать это с помощью joda). Это может быть полезно для тестирования кода, чувствительного ко времени, например, кеша с истечением срока действия или класса, который планирует события в будущем.
бакар

2
@bacar Clock - абстрактный класс, вы можете создать свою собственную тестовую реализацию Clock
Bjarne Boström

Я считаю, что мы в итоге и сделали.
bacar

25

Я использовал новый класс, чтобы скрыть Clock.fixedсоздание и упростить тесты:

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
Как насчет потоковой безопасности, если несколько тестов выполняются параллельно и изменяют часы TimeMachine?
youri

Вы должны передать часы тестируемому объекту и использовать их при вызове методов, связанных со временем. И вы можете удалить getClock()метод и напрямую использовать поле. Этот метод не добавляет ничего, кроме нескольких строк кода.
deamon

1
Banktime или TimeMachine?
Emanuele

11

Я использовал поле

private Clock clock;

а потом

LocalDate.now(clock);

в моем производственном коде. Затем я использовал Mockito в своих модульных тестах, чтобы имитировать часы с помощью Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Издевательство:

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

Утверждение:

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

9

Я считаю, Clockчто ваш производственный код загроможден.

Вы можете использовать JMockit или PowerMock для имитации вызовов статических методов в тестовом коде. Пример с 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);
}

РЕДАКТИРОВАТЬ : после прочтения комментариев к ответу Джона Скита на аналогичный вопрос здесь, ТАК, я не согласен со своим прошлым. Больше всего на свете этот аргумент убедил меня в том, что нельзя парализовать тесты, когда вы имитируете статические методы.

Тем не менее, вы можете / должны использовать статические имитации, если вам приходится иметь дело с устаревшим кодом.


1
+1 за комментарий "использование статического имитации для устаревшего кода". Итак, для нового кода рекомендуется использовать внедрение зависимостей и внедрить часы (фиксированные часы для тестов, системные часы для производственной среды выполнения).
Дэвид Грумс

1

Мне нужен LocalDateэкземпляр вместо LocalDateTime.
По этой причине я создал следующий служебный класс:

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

И его использование похоже на:

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

Вывод:

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

Заменены все творение , LocalDate.now()чтобы Clock.getCurrentDate()в проекте.

Потому что это приложение для весенней загрузки . Перед 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);
        }
    }
}

И добавьте в spring.factories :

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


1

Вот рабочий способ переопределить текущее системное время на определенную дату для целей тестирования JUnit в веб-приложении Java 8 с EasyMock

Joda Time действительно хорош (спасибо, Стивен, Брайан, вы сделали наш мир лучше), но мне не разрешили его использовать.

После некоторых экспериментов я в конце концов придумал способ имитировать время до определенной даты в Java 8 java.time API с EasyMock.

  • Без Joda Time API
  • Без PowerMock.

Вот что нужно сделать:

Что нужно сделать в тестируемом классе

Шаг 1

Добавьте новый java.time.Clockатрибут к тестируемому классу MyServiceи убедитесь, что новый атрибут будет правильно инициализирован со значениями по умолчанию с блоком создания экземпляра или конструктором:

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

Шаг 2

Вставьте новый атрибут clockв метод, который вызывает текущую дату и время. Например, в моем случае мне пришлось выполнить проверку того, произошла ли ранее дата, хранящаяся в базе данных LocalDateTime.now(), которую я заменил 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();
    }
  }
  // (...) 
}

Что нужно сделать в тестовом классе

Шаг 3

В тестовом классе создайте объект фиктивных часов и вставьте его в экземпляр тестируемого класса непосредственно перед вызовом тестируемого метода doExecute(), а затем сбросьте его обратно сразу после этого, например:

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

Проверьте его в режиме отладки, и вы увидите, что дата 3 февраля 2017 года была правильно введена в myServiceэкземпляр и использована в инструкции сравнения, а затем была правильно сброшена на текущую дату с помощью initDefaultClock().


0

В этом примере даже показано, как объединить Instant и LocalTime ( подробное объяснение проблем с преобразованием )

Тестируемый класс

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

}

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

С помощью PowerMockito для теста весенней загрузки вы можете издеваться над ZonedDateTime . Вам понадобится следующее.

Аннотации

В тестовом классе вам необходимо подготовить сервис, использующий расширение ZonedDateTime.

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

Прецедент

В тесте вы можете подготовить желаемое время и получить его в ответ на вызов метода.

  @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 
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.