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

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

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

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

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

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

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

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

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

Ответы [ 11 ]

103 голосов
/ 04 января 2010

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

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

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

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

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

61 голосов
/ 26 октября 2014

ТЛ; др

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

Да.

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

Clock В java.time

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

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

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

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

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

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

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

Ложные часы

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

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

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

image

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 310 .

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

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

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

42 голосов
/ 04 января 2010

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

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

15 голосов
/ 02 января 2013

Хотя использование некоторого шаблона 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=43

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

7 голосов
/ 03 апреля 2010

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

5 голосов
/ 04 января 2010

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

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

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

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

3 голосов
/ 21 августа 2017

Рабочий способ переопределения текущего системного времени для целей тестирования 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 голосов
/ 04 января 2010

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

2 голосов
/ 24 июня 2012

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

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

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

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

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

1 голос
/ 03 ноября 2017

Если вы работаете в 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

...