Переопределите Java System.currentTimeMillis для тестирования чувствительного ко времени кода
Есть ли способ, в коде или с аргументами JVM, переопределить текущее время, как представлено через System.currentTimeMillis
кроме ручного изменения системных часов на хост-машине?
Немного предыстории:
У нас есть система, которая запускает несколько заданий бухгалтерского учета, которые вращают большую часть своей логики вокруг текущей даты (то есть 1-го числа месяца, 1-го числа года и т. Д.)
К сожалению, многие устаревшие коды вызывают такие функции, как new Date()
или же Calendar.getInstance()
оба из которых в конечном итоге призывают к System.currentTimeMillis
,
В целях тестирования прямо сейчас мы застряли с ручным обновлением системных часов, чтобы манипулировать временем и датой, когда код считает, что тест выполняется.
Итак, мой вопрос:
Есть ли способ переопределить то, что возвращается System.currentTimeMillis
? Например, чтобы сказать JVM автоматически добавлять или вычитать некоторое смещение перед возвратом из этого метода?
Заранее спасибо!
13 ответов
Я настоятельно рекомендую вместо того, чтобы возиться с системными часами, вы кусаете пулю и реорганизуете этот устаревший код, чтобы использовать сменные часы. В идеале это должно быть сделано с помощью внедрения зависимостей, но даже если вы используете сменный синглтон, вы получите тестируемость.
Это может быть практически автоматизировано с помощью поиска и замены для синглтон-версии:
- замещать
Calendar.getInstance()
сClock.getInstance().getCalendarInstance()
, - замещать
new Date()
сClock.getInstance().newDate()
- замещать
System.currentTimeMillis()
сClock.getInstance().currentTimeMillis()
(и т. д. по мере необходимости)
Сделав этот первый шаг, вы можете постепенно заменять синглтон на DI.
ТЛ; др
Есть ли способ, либо в коде, либо с аргументами 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, а также наносекундах (в зависимости от вашего оборудования). Вы можете запросить отчет о текущем моменте с другой детализацией.
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 );
Используйте эти специальные фиксированные часы, чтобы всегда возвращать один и тот же момент. Мы получаем первый момент Рождества в Киритимати, когда 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 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
и многое другое.
Как сказал Джон Скит:
"использовать 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 Clock, который обеспечит реализацию, а также стандартный интерфейс. Полная банка составляет всего 96 КБ, поэтому она не должна сломать банк...
Хотя использование некоторого шаблона 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). Определенно нельзя обновить до последней версии, где интерфейс полностью отличается. Я думал о том, чтобы включить только необходимые вещи, но так как нам не нужна эта вещь в наших новых проектах, я вообще не развиваю эту вещь.
Powermock прекрасно работает. Просто использовал это, чтобы издеваться System.currentTimeMillis()
,
Используйте Aspect-Oriented Programming (AOP, например, AspectJ), чтобы создать класс System для возврата предопределенного значения, которое вы можете установить в своих тестовых примерах.
Или переплетите классы приложения, чтобы перенаправить вызов System.currentTimeMillis()
или new Date()
в другой собственный класс полезности.
Классы ткацкой системы (java.lang.*
), однако, немного сложнее, и вам может потребоваться выполнить автономное плетение для rt.jar и использовать отдельный JDK/rt.jar для своих тестов.
Это называется бинарным ткачеством, и есть также специальные инструменты для выполнения ткачества системных классов и обхода некоторых проблем с этим (например, загрузка виртуальной машины может не работать)
Рабочий способ переопределить текущее системное время для целей тестирования 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()
,
Можете ли вы протестировать запуск в экземпляре виртуальной машины ОС без повторного факторинга?
На самом деле не существует способа сделать это непосредственно в виртуальной машине, но вы могли бы каким-то образом программно установить системное время на тестовой машине. Большинство (все?) ОС имеют команды командной строки для этого.
На мой взгляд, может работать только неинвазивное решение. Особенно, если у вас есть внешние библиотеки и большая база унаследованного кода, нет надежного способа измерить время.
JMockit... работает только ограниченное количество раз
PowerMock & Co ... должен насмехаться над клиентами в System.currentTimeMillis(). Опять инвазивный вариант.
Из этого я только вижу, что упомянутый подход javaagent или aop прозрачен для всей системы. Кто-нибудь сделал это и может указать на такое решение?
@jarnbjo: не могли бы вы показать код javaagent, пожалуйста?
Вот пример использования PowerMockito. Также есть пример с new Date().
Подробнее о мокирующих системных классах .
@RunWith(PowerMockRunner.class)
@PrepareForTest(LegacyClass.class)
public class SystemTimeTest {
private final Date fakeNow = Date.from(Instant.parse("2010-12-03T10:15:30.00Z"));
@Before
public void init() {
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.currentTimeMillis()).thenReturn(fakeNow.getTime());
System.out.println("Fake currentTimeMillis: " + System.currentTimeMillis());
}
@Test
public void legacyClass() {
new LegacyClass().methodWithCurrentTimeMillis();
}
}
Некоторый устаревший класс, который вы тестируете:
class LegacyClass {
public void methodWithCurrentTimeMillis() {
long now = System.currentTimeMillis();
System.out.println("LegacyClass System.currentTimeMillis() is " + now);
}
}
Консольный вывод
Fake currentTimeMillis: 1291371330000
LegacyClass System.currentTimeMillis() is 1291371330000
Если вы работаете в 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 во время написания.
Если вы хотите изменить время управляемой системой службы, просто добавьте следующее в переопределения файлов вашего модуля, например, для asticsearch это будет /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
Если вы хотите издеваться над методом, имеющим System.currentTimeMillis()
аргумент, то вы можете передать anyLong()
класса Matchers в качестве аргумента.
PS Я могу успешно выполнить мой тестовый пример, используя описанный выше трюк, и просто поделиться более подробной информацией о моем тесте, в котором я использую платформы PowerMock и Mockito.