Переопределить Java System.currentTimeMillis для проверки кода с временным кодом

Есть ли способ, как в коде, так и с аргументами JVM, переопределять текущее время, представленное через System.currentTimeMillis, кроме ручной смены системных часов на главной машине?

Немного фона:

У нас есть система, которая запускает ряд учетных заданий, которые вращают большую часть их логики в течение текущей даты (т.е. 1-го числа месяца, 1-го года и т.д.).

К сожалению, многие из устаревших кодов вызывают такие функции, как new Date() или Calendar.getInstance(), оба из которых в конечном итоге вызывают System.currentTimeMillis.

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

Итак, мой вопрос:

Есть ли способ переопределить то, что возвращается System.currentTimeMillis? Например, чтобы сообщить JVM автоматически добавлять или вычитать некоторое смещение перед возвратом из этого метода?

Спасибо заранее!

Ответ 1

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

Это может быть почти автоматизировано с помощью поиска и замены для версии singleton:

  • Замените Calendar.getInstance() на Clock.getInstance().getCalendarInstance().
  • Замените new Date() на Clock.getInstance().newDate()
  • Замените System.currentTimeMillis() на Clock.getInstance().currentTimeMillis()

(и т.д. по мере необходимости)

После того, как вы сделали этот первый шаг, вы можете поочередно заменять singleton на DI.

Ответ 2

ТЛ; др

Есть ли способ, как в коде, так и с аргументами JVM, переопределять текущее время, представленное через System.currentTimeMillis, кроме ручного изменения системных часов на главной машине?

Да.

Instant.now( 
    Clock.fixed( 
        Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
    )
)

Clock в java.time

У нас есть новое решение проблемы замены сменных часов, чтобы облегчить тестирование с использованием фальшивых дат-времени. Пакет java.time в Java 8 включает абстрактный класс java.time.Clock с явной целью:

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

Вы можете подключить свою собственную реализацию Clock, хотя вы, вероятно, можете найти тот, который уже был выполнен в соответствии с вашими потребностями. Для вашего удобства, java.time включает статические методы для создания специальных реализаций. Эти альтернативные реализации могут быть полезными во время тестирования.

Измененная каденция

Различные tick… методы производят часы, которые увеличивают текущий момент с другой каденцией.

По умолчанию Clock сообщает о времени, обновляемом так же часто, как миллисекунды в Java 8, а в Java 9 - как наносекунды (в зависимости от вашего оборудования). Вы можете запросить, чтобы текущий текущий момент сообщался с другой детализацией.

Ложные часы

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

  • fixed - Сообщает об одном неизменяемом (неинкрементном) моменте в качестве текущего момента.
  • offset - Сообщает текущий момент, но сдвигается по аргументу Duration.

Например, замок в первый момент самого раннего Рождества в этом году. другими словами, когда Санта и его северный олень делают свою первую остановку. Самым ранним часовым поясом в настоящее время является Pacific/Kiritimati +14:00 в +14:00.

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );

Используйте эти специальные фиксированные часы, чтобы всегда возвращать тот же самый момент. Мы получаем первый момент рождественского дня в Киритимати, с UTC, показывающим часы настенных часов четырнадцать часов назад, 10 часов утра в предыдущую дату 24 декабря.

500px-Kiribati_on_the_globe_%28Polynesia_centered%29.svg.png

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

instant.toString(): 2016-12-24T10: 00: 00Z

zdt.toString(): 2016-12-25T00: 00 +14: 00 [Pacific/Kiritimati]

Смотрите код в IdeOne.com.

Истинное время, разный часовой пояс

Вы можете контролировать, какой часовой пояс назначается реализацией Clock. Это может быть полезно при некоторых тестах. Но я не рекомендую это в производственном коде, где вы должны всегда явно указывать необязательные аргументы ZoneId или ZoneOffset.

Вы можете указать, что UTC будет зоной по умолчанию.

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );

Вы можете указать любой часовой пояс. Укажите правильное название часового пояса в формате continent/region, например, America/Montreal, Africa/Casablanca или Pacific/Auckland. Никогда не используйте аббревиатуру 3-4 букв, такую как EST или IST поскольку они не являются настоящими часовыми поясами, не стандартизированы и даже не уникальны (!).

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

Вы можете указать JVM, текущий часовой пояс по умолчанию должен быть по умолчанию для конкретного объекта Clock.

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

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

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles была JVM текущей зоной по умолчанию на компьютере, который запускал этот код.

zdtClockSystemUTC.toString(): 2016-12-31T20: 52: 39.688Z

zdtClockSystem.toString(): 2016-12-31T15: 52: 39.750-05: 00 [Америка/Монреаль]

zdtClockSystemDefaultZone.toString(): 2016-12-31T12: 52: 39.762-08: 00 [America/Los_Angeles]

Класс Instant всегда находится в UTC по определению. Таким образом, эти три связанные с зоной Clock используются точно так же.

Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );

instantClockSystemUTC.toString(): 2016-12-31T20: 52: 39.763Z

instantClockSystem.toString(): 2016-12-31T20: 52: 39.763Z

instantClockSystemDefaultZone.toString(): 2016-12-31T20: 52: 39.763Z

Часы по умолчанию

Реализация, используемая по умолчанию для Instant.now является той, которая была возвращена Clock.systemUTC(). Это реализация, используемая, когда вы не указываете Clock. Посмотрите сами в предварительном выпуске Java 9 исходного кода для Instant.now.

public static Instant now() {
    return Clock.systemUTC().instant();
}

Clock по умолчанию для OffsetDateTime.now и ZonedDateTime.now является Clock.systemDefaultZone(). См. Исходный код.

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

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


О java.time

Рамка java.time встроена в Java 8 и более поздние версии. Эти классы вытесняют неприятные старые устаревшие классы времени, такие как java.util.Date, Calendar и SimpleDateFormat.

Проект Joda-Time, теперь в режиме обслуживания, советует перейти на классы java.time.

Чтобы узнать больше, ознакомьтесь с учебным пособием Oracle. И поиск Qaru для многих примеров и объяснений. Спецификация - JSR 310.

Вы можете обменивать объекты java.time непосредственно с вашей базой данных. Используйте JDBC-драйвер, совместимый с JDBC 4.2 или новее. Нет необходимости в строках, нет необходимости в java.sql.*.

Где можно получить классы java.time?

  • Java SE 8, Java SE 9 и более поздние версии
    • Встроенный.
    • Часть стандартного Java API с интегрированной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональных возможностей java.time включена обратно в Java 6 и 7 в ThreeTen-Backport.
  • Android
    • Более поздние версии реализаций пакетов Android классов java.time.
    • Для более ранних Android (<26), то ThreeTenABP проект адаптирует ThreeTen-Backport (упоминалось выше). См. Раздел Как использовать ThreeTenABP....

Проект ThreeTen-Extra расширяет java.time с дополнительными классами. Этот проект является доказательством возможных будущих дополнений к java.time. Здесь вы можете найти полезные классы, такие как Interval, YearWeek, YearQuarter и другие.

Ответ 3

Как сказал Джон Скит:

"Использовать время Joda" почти всегда является лучшим ответом на любой вопрос, связанный с "как мне достичь X с помощью java.util.Date/Calendar?"

Итак, здесь (предположим, что вы только что заменили все new Date() на new DateTime().toDate())

//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();

Если вы хотите импортировать библиотеку с интерфейсом (см. комментарий Джона ниже), вы можете просто использовать Prevayler Clock, который обеспечит реализацию а также стандартный интерфейс. Полная банка составляет всего 96 КБ, поэтому она не должна ломать банк...

Ответ 4

При использовании некоторого шаблона DateFactory кажется приятным, он не охватывает библиотеки, которые вы не можете контролировать, - представьте аннотацию проверки. @Past с реализацией, основанной на System.currentTimeMillis(есть такая).

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

import mockit.Mock;
import mockit.MockClass;
...
@MockClass(realClass = System.class)
public static class SystemMock {
    /**
     * Fake current time millis returns value modified by required offset.
     *
     * @return fake "current" millis
     */
    @Mock
    public static long currentTimeMillis() {
        return INIT_MILLIS + offset + millisSinceClassInit();
    }
}

Mockit.setUpMock(SystemMock.class);

Поскольку невозможно получить исходное незафиксированное значение миллиса, вместо этого мы используем nano timer - это не связано с настенными часами, но здесь достаточно относительного времени:

// runs before the mock is applied
private static final long INIT_MILLIS = System.currentTimeMillis();
private static final long INIT_NANOS = System.nanoTime();

private static long millisSinceClassInit() {
    return (System.nanoTime() - INIT_NANOS) / 1000000;
}

Имеется документально подтвержденная проблема: при использовании HotSpot время возвращается к нормальному состоянию после нескольких вызовов - вот отчет о проблеме: http://code.google.com/p/jmockit/issues/detail?id=43

Чтобы преодолеть это, мы должны включить одну конкретную оптимизацию HotSpot - запустить JVM с этим аргументом -XX:-Inline.

Хотя это может быть не идеально для производства, это просто отлично подходит для тестов, и оно абсолютно прозрачно для приложений, особенно когда DataFactory не имеет делового смысла и вводится только из-за тестов. Было бы неплохо иметь встроенную возможность JVM для запуска в разное время, слишком плохо, что это невозможно без таких хаков.

Полная история в моем блоге: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

В сообщении указан полный набор SystemTimeShifter. Класс может использоваться в ваших тестах, или его можно использовать в качестве первого основного класса до вашего реального основного класса очень легко, чтобы запустить приложение (или даже целый сервер приложений) в другое время. Конечно, это предназначено для целей тестирования в основном, а не для производственной среды.

EDIT Июль 2014: JMockit сильно изменился в последнее время, и вы должны использовать JMockit 1.0 для правильного использования (IIRC). Определенно не может перейти на новую версию, где интерфейс полностью отличается. Я думал о том, чтобы вложить только необходимые вещи, но поскольку мы не нуждаемся в этом в наших новых проектах, я вообще не разрабатываю эту вещь.

Ответ 5

Powermock отлично работает. Просто использовал его, чтобы высмеять System.currentTimeMillis().

Ответ 6

Используйте аспектно-ориентированное программирование (AOP, например AspectJ), чтобы сплести класс System, чтобы вернуть предопределенное значение, которое вы могли бы установить в своих тестовых случаях.

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

Тем не менее, системные классы ткачества (java.lang.*) немного сложнее, и вам может потребоваться выполнить автономное ткачество для rt.jar и использовать отдельные JDK/rt.jar для ваших тестов.

Он называется Binary weaving, а также специальные инструменты для выполнения плетения классов системы и обхода некоторых проблем с помощью что (например, перезагрузка VM может не работать)

Ответ 7

На самом деле не существует способа сделать это непосредственно на виртуальной машине, но вы можете все что-то программировать на системном времени на тестовой машине. Большинство (все?) ОС имеют команды командной строки для этого.

Ответ 8

Без повторного факторинга вы можете протестировать запуск в экземпляре виртуальной машины ОС?

Ответ 9

По-моему, только неинвазивное решение может работать. Особенно, если у вас есть внешние библиотеки и большая база устаревших кодов, нет надежного способа израсходовать время.

JMockit... работает только для ограниченного числа times

PowerMock и Co... должны издеваться над клиентами в System.currentTimeMillis(). Опять инвазивный вариант.

Из этого я вижу, что упомянутый подход javaagent или aop прозрачен для всей системы. Кто-нибудь сделал это и мог указать на такое решение?

@jarnbjo: не могли бы вы показать код javaagent?

Ответ 10

Рабочий способ переопределить текущее системное время для целей тестирования JUnit в веб-приложении Java 8 с помощью EasyMock без времени Joda и без 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 в метод, который вызывает текущую дату-время. Например, в моем случае мне пришлось выполнить проверку того, произошла ли дата, хранящаяся в dataase, до 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

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

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;
  private int month = 2;
  private int day = 3;

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

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

Ответ 11

Если вы используете Linux, вы можете использовать основную ветвь libfaketime или во время тестирования commit 4ce2835.

Просто установите переменную окружения со временем, когда вы хотите издеваться над своим java-приложением, и запустите его, используя ld-preloading:

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar

Вторая переменная окружения имеет первостепенное значение для приложений Java, которые в противном случае замерзли бы. Это требует главной ветки libfaketime на момент написания.

Если вы хотите изменить время работы управляемой службы systemd, просто добавьте следующее к своим переопределениям файлов файлов, например. для elasticsearch это будет /etc/systemd/system/elasticsearch.service.d/override.conf:

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

Не забудьте перезагрузить systemd с помощью `systemctl daemon-reload

Ответ 12

Если вы хотите высмеять метод с аргументом System.currentTimeMillis(), то вы можете передать anyLong() класса Matchers в качестве аргумента.

P.S. Я могу успешно запустить свой тестовый пример с помощью вышеупомянутого трюка и просто поделиться более подробными сведениями о своем тесте, который я использую в каркасах PowerMock и Mockito.