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

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

1. обзор

В этом кратком руководстве мы сосредоточимся наdifferent ways to override the system time for testing.

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

Для введения в использованиеJava Clock см.to this article here. Или, используя AspectJ,here.

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

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

Для достижения наших целейthe above library includes static methods to yield special implementations. Мы собираемся использовать два из них, которые возвращают неизменяемую, поточно-ориентированную и сериализуемую реализацию.

Первый -fixed. Исходя из этого,we can obtain a Clock that always returns the same*Instant*, обнаруживает, что тесты не зависят от текущих часов.

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

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

The second static method is offset. В этом случае часы обертывают другие часы, что делает его возвращаемым объектом, способным получать моменты, которые являются более поздними или более ранними по указанной длительности.

Другими словами,it’s possible to simulate running in the future, in the past, or in any arbitrary point in time:

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. Использование аспектно-ориентированного программирования

Другой способ переопределить системное время - АОП. При таком подходеwe’re able to weave the System class to return a predefined value which we can set within our test cases.

Кроме того, можно настроить классы приложений для перенаправления вызова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;
      }
}

В приведенном выше примереwe’re catching every call to System.currentTimeMillis() inside a specified package, в данном случаеuser.code.base.pckg., * и возвращает ноль каждый раз, когда происходит это событие.

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

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

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

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

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

Давайте посмотрим, как можно изменить его поведение при тестировании.

4.1. Перегрузкаnow() сClock

Мы можем перегрузить методnow() фиксированным экземпляромClock. Many of the classes in the java.time package have a now() method that takes a Clock parameter, что делает этот подход нашим предпочтительным:

@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. ИспользуяPowerMock

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

@RunWith(PowerMockRunner.class)
@PrepareForTest({ Instant.class })
public class InstantUnitTest {
    @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);
        mockStatic(Instant.class);
        when(Instant.now()).thenReturn(instant);

        Instant now = Instant.now();

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

4.3. С помощью JMockit

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

JMockit предлагает нам два способаmocking a static method. Один из них использует класс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() {
        @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.

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