覆盖Java System.currentTimeMillis以testing对时间敏感的代码

有没有办法,无论是在代码或JVM参数,通过System.currentTimeMillis覆盖当前时间,而不是手动更改主机上的系统时钟?

一点背景:

我们有一个系统,它运行许多会计工作,围绕当前date(即本月的第一个月,第一年等)

不幸的是,许多遗留代码调用函数,如新的Date()或Calendar.getInstance(),两者最终都会调用System.currentTimeMillis。

出于testing的目的,现在我们不得不手动更新系统时钟来操纵代码认为testing正在运行的时间和date。

所以我的问题是:

有没有办法来重写由System.currentTimeMillis返回的内容? 例如,告诉JVM在从该方法返回之前自动添加或减去一些偏移量?

提前致谢!

强烈build议您不要混淆系统时钟,而是咬紧牙关,重构旧代码以使用可replace的时钟。 理想情况下应该用dependency injection来完成,但是即使你使用了一个可replace的单例,你也会获得可测性。

这几乎可以通过search自动完成,并replace为单例版本:

  • Clock.getInstance().getCalendarInstance()replace为Clock.getInstance().getCalendarInstance()
  • Clock.getInstance().newDate()replacenew Date() Clock.getInstance().newDate()
  • Clock.getInstance().currentTimeMillis()replaceSystem.currentTimeMillis() Clock.getInstance().currentTimeMillis()

(如需要)

一旦你采取了第一步,你可以用DI一次一个地replace单例。

正如Jon Skeet所说 :

“使用Joda时间”几乎总是对涉及“如何使用java.util.Date/Calendar实现X”的任何问题的最佳答案。

所以这里(假设你已经用new DateTime().toDate() )replace了所有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(); 

如果你想导入一个具有接口的库(参见下面的Jon的注释),你可以使用Prevayler的Clock ,它将提供实现以及标准接口。 满jar只有96kB,所以不应该打破银行…

TL;博士

有没有办法,无论是在代码或JVM参数,通过System.currentTimeMillis覆盖当前时间,而不是手动更改主机上的系统时钟?

是。

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

Clock在java.time

我们有一个新的解决scheme来解决可插拔时钟replace问题,以便于用date值进行testing。 Java 8中的java.time包包含一个抽象类java.time.Clock ,具有明确的目的:

以允许在需要时插入备用时钟

你可以插入你自己的Clock实现,虽然你可能会find一个已经满足你的需求。 为了您的方便,java.time包含静态方法来产生特殊的实现。 这些替代实现在testing过程中可能很有价值

改变节奏

各种tick…方法产生时钟,以不同的节奏递增当前时刻。

默认的Clock报告在Java 8和Java 9中毫秒 级 (取决于你的硬件)的更新时间经常更新。 您可以要求以不同的粒度报告真实的当前时刻。

  • tickSeconds – 整秒内递增
  • tickMinutes – 在整个分钟内增加
  • tick – 通过传递的Duration参数递增。

假时钟

有些时钟可以说谎,产生一个不同于主机OS硬件时钟的结果。

  • fixed – 报告当前时刻一个不变的(不递增的)时刻。
  • offset – 报告当前时刻,但通过传递的Duration参数移位。

例如,locking在今年最早的圣诞节的第一刻。 换句话说,当圣诞老人和他的驯鹿第一站 。 现今最早的时区似乎是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)举行圣诞节,联合技术公司在十二月二十四号上午十点的十四时之前,上午十点钟,时间已经到了。

 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实现分配的时区。 这在一些testing中可能是有用的。 但是我不build议在生产代码中这样做,你应该总是明确指定可选的ZoneIdZoneOffset参数。

您可以指定UTC是默认区域。

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

您可以指定任何特定的时区。 以America/MontrealAfrica/CasablancaPacific/Aucklandcontinent/region的格式指定适当的时区名称 。 切勿使用3-4字母缩写(如ESTIST因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

 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 [America / Montreal]

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.systemUTC() 。 这是您不指定Clock时使用的实现。 请参阅Instant.now预发行Java 9源代码 。

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

OffsetDateTime.nowZonedDateTime.now的默认ClockClock.systemDefaultZone() 。 看源代码 。

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

在Java 8和Java 9之间,缺省实现的行为发生了变化。在Java 8中,尽pipe类存储了纳秒级的分辨率,但是当前时刻仅以毫秒级的分辨率捕获。 Java 9带来了一种新的实现方式,能够以毫微秒的分辨率捕捉当前时刻 – 当然,这取决于计算机硬件时钟的能力。

关于java.time

java.time框架内置于Java 8及更高版本中。 这些类取代了麻烦的旧date时间类,如java.util.Date.Calendarjava.text.SimpleDateFormat

Joda-Time项目现在处于维护模式 ,build议迁移到java.time。

要了解更多信息,请参阅Oracle教程 。 并search堆栈溢出了很多例子和解释。

大部分的java.timefunction在ThreeTen-Backport中移植到Java 6&7中,并进一步适用于ThreeTenABP中的 Android (请参阅如何使用… )。

ThreeTen-Extra项目将java.time扩展到其他类。 这个项目是未来可能增加java.time的一个试验场。 你可能会在这里find一些有用的类,比如IntervalYearWeekYearQuarter 等等 。

虽然使用一些DateFactory模式看起来不错,但它不包括你无法控制的库 – 想象一下依赖于System.currentTimeMillis的Validation注解@Past(有这样的)。

这就是为什么我们使用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); 

因为不可能达到millis的原始unmocked值,所以我们使用nano定时器 – 这与挂钟无关,但相对时间在这里足够了:

 // 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; } 

有logging在案的问题,HotSpot时间会在多次通话后恢复正常 – 这是问题报告: http : //code.google.com/p/jmockit/issues/detail?id=43

为了克服这个问题,我们必须打开一个特定的HotSpot优化 – 使用这个参数-XX:-Inline运行JVM。

虽然这对于生产来说可能并不完美,但它对于testing来说是很好的select,对于应用程序来说是绝对透明的,特别是当DataFactory不具有商业意义并且仅仅因为testing而被引入时。 这将是很好的内置JVM选项运行在不同的时间,太糟糕了,不可能没有这样的黑客。

完整的故事在这里我的博客文章: http : //virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

在这篇文章中提供了完整的方便的类SystemTimeShifter。 类可以用在你的testing中,或者它可以作为你的真正的主类之前的第一个主类非常容易,以便在不同的时间运行你的应用程序(甚至整个应用程序服务器)。 当然,这主要是为了testing目的,而不是为了生产环境。

编辑2014年7月:JMockit最近改变了很多,你必须使用JMockit 1.0正确使用(IIRC)。 绝对不能升级到界面完全不同的最新版本。 我正在考虑内嵌必要的东西,但是因为我们在新项目中不需要这个东西,所以我根本就没有开发这个东西。

Powermock很好。 只是用它来模拟System.currentTimeMillis()

使用面向方面的编程(AOP,例如AspectJ)编织System类以返回可以在testing用例中设置的预定义值。

或者编织应用程序类以将调用redirect到System.currentTimeMillis()new Date()到您自己的另一个实用程序类。

编织系统类( java.lang.* )会稍微复杂一点,您可能需要为rt.jar执行离线编织,并为您的testing使用单独的JDK / rt.jar。

这就是所谓的二进制编织 ,也有特殊的工具来执行编织系统类,并绕过一些问题(例如,引导虚拟机可能无法正常工作)

实际上没有办法直接在虚拟机中执行此操作,但是您可以通过编程的方式在testing机器上设置系统时间。 大多数(所有?)操作系统都有命令行命令来执行此操作。

不做重新分解,你可以testing运行在操作系统的虚拟机实例吗?

在我看来,只有一个非侵入性的解决scheme可以工作。 特别是如果你有外部库和一个大的遗留代码库,没有可靠的方法来嘲笑时间。

JMockit …只能使用受限制的次数

PowerMock&Co …需要将客户端模拟到System.currentTimeMillis()。 再次是侵入性的select。

由此我只能看到提及的javaagentaop方法对整个系统是透明的。 有没有人这样做,可以指出这样的解决scheme?

@ jarnbjo:你能显示一些javaagent代码吗?

一个工作方式来覆盖当前的系统时间JUnittesting目的在一个Java 8的Web应用程序与EasyMock,没有乔达时间,没有PowerMock。

这是你需要做的:

在被testing的类中需要做什么

步骤1

添加一个新的java.time.Clock属性到被testing的类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注入到调用当前date时间的方法中。 例如,在我的情况下,我不得不执行检查数据库中存储的date是否发生在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(); } } // (...) } 

testing课上需要做什么?

第3步

在testing类中,创build一个模拟时钟对象,然后在调用testing方法doExecute()之前将其注入到被testing类的实例中,然后将其重置,如下所示:

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

在debugging模式下检查它,你会看到2017年2月3日的date已经被正确地注入myService实例并用于比较指令,然后通过initDefaultClock()正确地重置为当前date。

如果你想模拟具有System.currentTimeMillis()参数的方法,那么你可以传递anyLong()类的anyLong()作为参数。

PS我能够使用上述技巧成功地运行我的testing案例,只是为了分享我使用PowerMock和Mockito框架的testing的更多细节。

如果您正在运行Linux,则可以使用libfaketime的主分支,或者在testing提交4ce2835时使用 。

简单地设置环境variables的时候,你想嘲笑你的Java应用程序,并运行它使用ld预载:

 # 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 

第二个环境variables对于Java应用程序是至关重要的,否则它将冻结。 它在编写时需要libfaketime的主分支。

如果您想更改systemd托pipe服务的时间,只需将以下内容添加到您的单元文件覆盖,例如对于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" 

不要忘记使用`systemctl守护进程 – 重装加载systemd'