it-swarm.com.ru

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

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

Немного предыстории:

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

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

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

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

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

Заранее спасибо!

114
Mike Clark

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

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

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

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

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

102
Jon Skeet

тЛ; др

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

Да.

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

Clock в Java.time

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

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

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

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

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

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

  • tickSeconds - Увеличение в целых секундах
  • tickMinutes - Увеличение в целых минутах
  • tick - Увеличивается на переданный Duration аргумент.

Ложные часы

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

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

Например, замок в первый момент самого раннего Рождества в этом году. другими словами, когда Санта Клаус и его олени делают первую остановку . Самый ранний часовой пояс в настоящее время, кажется, Pacific/Kiritimati at +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 );

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

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 Tutorial . И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 31 .

Вы можете обмениваться объектами 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 и more .

60
Basil Bourque

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

"использовать Joda Time" почти всегда является лучшим ответом на любой вопрос, касающийся "как мне достичь 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's , что обеспечит реализацию, а также стандартный интерфейс. Полная банка составляет всего 96 КБ, поэтому она не должна сломать банк ...

42
Stephen

Хотя использование некоторого шаблона DateFactory кажется приятным, он не охватывает библиотеки, которыми вы не можете управлять - представьте аннотацию Validation @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);

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

// 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= 4

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

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

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

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

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

15
virgo47

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

7
ReneS

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

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

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

Он называется Бинарное плетение , и есть также специальные инструменты для выполнения плетения классов System и обхода некоторых проблем с этим (например, при начальной загрузке). VM может не работать)

5
mhaller

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

3
Jeremy Raymond

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

3
KiriSakow

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

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

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

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

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

2
user1477398

Если вы работаете в Linux, вы можете использовать основную ветку libfaketime или во время коммита тестирования 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 во время написания.

Если вы хотите изменить время управляемой системой службы, просто добавьте следующее в переопределения файлов вашего модуля, например, для упругого поиска это будет /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

1
faxmodem