覆盖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议在生产代码中这样做,你应该总是明确指定可选的ZoneId
或ZoneOffset
参数。
您可以指定UTC是默认区域。
ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );
您可以指定任何特定的时区。 以America/Montreal
, Africa/Casablanca
, Pacific/Auckland
等continent/region
的格式指定适当的时区名称 。 切勿使用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 [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.now
和ZonedDateTime.now
的默认Clock
是Clock.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
, .Calendar
和java.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一些有用的类,比如Interval
, YearWeek
, YearQuarter
等等 。
虽然使用一些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。
由此我只能看到提及的javaagent或aop方法对整个系统是透明的。 有没有人这样做,可以指出这样的解决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'