Перейти к основному содержимому

Переопределение системного времени для тестирования в Java

· 5 мин. чтения

1. Обзор

В этом кратком руководстве мы сосредоточимся на различных способах переопределения системного времени для тестирования .

Иногда в нашем коде есть логика вокруг текущей даты. Возможно, некоторые вызовы функций, такие как new Date() или Calendar.getInstance() , которые в конечном итоге будут вызывать System.CurrentTimeMillis .

Для ознакомления с использованием Java Clock обратитесь к этой статье здесь . Или, для использования AspectJ, здесь .

2. Использование часов в java.time

Пакет java.time в Java 8 включает абстрактный класс java.time.Clock , позволяющий подключать альтернативные часы по мере необходимости. При этом мы можем подключить нашу собственную реализацию или найти уже созданную для удовлетворения наших потребностей.

Для достижения наших целей указанная выше библиотека включает статические методы для создания специальных реализаций . Мы собираемся использовать два из них, которые возвращают неизменяемую, потокобезопасную и сериализуемую реализацию.

Первый зафиксирован . Из него мы можем получить Clock , который всегда возвращает один и тот же Instant , гарантируя, что тесты не зависят от текущих часов.

Чтобы использовать его, нам нужны Instant и ZoneOffset :

Instant.now(Clock.fixed( 
Instant.parse("2018-08-22T10:00:00Z"),
ZoneOffset.UTC))

Второй статический метод — offset . В этом случае часы оборачивают другие часы, что делает их возвращаемым объектом, способным получать моменты, которые позже или раньше на указанную продолжительность.

Другими словами, можно имитировать бег в будущем, в прошлом или в любой произвольный момент времени :

Clock constantClock = Clock.fixed(ofEpochMilli(0), ZoneId.systemDefault());

// go to the future:
Clock clock = Clock.offset(constantClock, Duration.ofSeconds(10));

// rewind back with a negative value:
clock = Clock.offset(constantClock, Duration.ofSeconds(-5));

// the 0 duration returns to the same clock:
clock = Clock.offset(constClock, Duration.ZERO);

С классом Duration можно манипулировать от наносекунд до дней. Кроме того, мы можем инвертировать длительность, что означает получение копии этой длительности с инвертированной длиной.

3. Использование аспектно-ориентированного программирования

Другой способ переопределить системное время — с помощью АОП. При таком подходе мы можем сплести класс System так , чтобы он возвращал предопределенное значение, которое мы можем установить в наших тестовых примерах .

Кроме того, можно объединить классы приложения для перенаправления вызова System.currentTimeMillis() или new Date() в другой собственный служебный класс.

Один из способов реализовать это — использовать AspectJ:

public aspect ChangeCallsToCurrentTimeInMillisMethod {
long around():
call(public static native long java.lang.System.currentTimeMillis())
&& within(user.code.base.pckg.*) {
return 0;
}
}

В приведенном выше примере мы перехватываем каждый вызов System.currentTimeMillis () внутри указанного пакета , которым в данном случае является user.code.base.pckg.* , и возвращаем ноль каждый раз, когда происходит это событие .

Именно в этом месте мы можем объявить нашу собственную реализацию для получения желаемого времени в миллисекундах.

Одним из преимуществ использования AspectJ является то, что он работает напрямую на уровне байт-кода, поэтому для его работы не требуется исходный код.

По этой причине нам не нужно было бы перекомпилировать его.

4. Насмешка над методом Instant.now()

Мы можем использовать класс Instant для представления мгновенной точки на временной шкале. Обычно мы можем использовать его для записи временных меток событий в нашем приложении. Метод now() этого класса позволяет нам получить текущий момент по системным часам в часовом поясе UTC.

Давайте посмотрим на некоторые альтернативы для изменения его поведения при тестировании.

4.1. Перегрузка сейчас() с часами

Мы можем перегрузить метод now() фиксированным экземпляром Clock . Многие классы в пакете java.time имеют метод now() , принимающий параметр Clock , что делает этот подход предпочтительным:

@Test
public void givenFixedClock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-22T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));

Instant instant = Instant.now(clock);

assertThat(instant.toString()).isEqualTo(instantExpected);
}

4.2. Использование Мокито

Кроме того, если нам нужно изменить поведение метода now() без отправки параметров, мы можем использовать Mockito :

@Test
public void givenInstantMock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-22T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
Instant instant = Instant.now(clock);

try (MockedStatic<Instant> mockedStatic = mockStatic(Instant.class)) {
mockedStatic.when(Instant::now).thenReturn(instant);
Instant now = Instant.now();
assertThat(now.toString()).isEqualTo(instantExpected);
}
}

4.3. Использование JMockit

В качестве альтернативы мы можем использовать библиотеку JMockit .

JMockit предлагает нам два способа имитации статического метода . Один использует класс MockUp :

@Test
public void givenInstantWithJMock_whenNow_thenGetFixedInstant() {
String instantExpected = "2014-12-21T10:15:30Z";
Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
new MockUp<Instant>() {
@Mock
public Instant now() {
return Instant.now(clock);
}
};

Instant now = Instant.now();

assertThat(now.toString()).isEqualTo(instantExpected);
}

А другой использует класс Expectations :

@Test
public void givenInstantWithExpectations_whenNow_thenGetFixedInstant() {
Clock clock = Clock.fixed(Instant.parse("2014-12-23T10:15:30.00Z"), ZoneId.of("UTC"));
Instant instantExpected = Instant.now(clock);
new Expectations(Instant.class) {
{
Instant.now();
result = instantExpected;
}
};

Instant now = Instant.now();

assertThat(now).isEqualTo(instantExpected);
}

5. Насмешка над методом LocalDateTime.now()

Еще одним полезным классом в пакете java.time является класс LocalDateTime . Этот класс представляет дату и время без часового пояса в календарной системе ISO-8601. Метод now() этого класса позволяет нам получить текущую дату и время по системным часам в часовом поясе по умолчанию.

Мы можем использовать те же альтернативы, чтобы издеваться над ним, как мы видели раньше. Например, перегрузка now() с фиксированным значением Clock :

@Test
public void givenFixedClock_whenNow_thenGetFixedLocalDateTime() {
Clock clock = Clock.fixed(Instant.parse("2014-12-22T10:15:30.00Z"), ZoneId.of("UTC"));
String dateTimeExpected = "2014-12-22T10:15:30";

LocalDateTime dateTime = LocalDateTime.now(clock);

assertThat(dateTime).isEqualTo(dateTimeExpected);
}

6. Заключение

В этой статье мы рассмотрели различные способы переопределения системного времени для тестирования. Сначала мы рассмотрели нативный пакет java.time и его класс Clock . Далее мы увидели, как применить аспект для создания класса System . Наконец, мы увидели различные альтернативы имитированию метода now() в классах Instant и LocalDateTime .

Как всегда, образцы кода можно найти на GitHub .