除了手动更改主机上的系统时钟之外,是否有一种方法(通过代码或使用 JVM 参数)来覆盖当前时间(如 System.currentTimeMillis 所示)?
一点背景知识:
我们有一个运行许多会计作业的系统,这些作业的大部分逻辑围绕当前日期(即每月的第一天、每年的第一天等),
不幸的是,许多遗留代码调用诸如此类的函数如 new Date()
或 Calendar.getInstance()
,两者最终都会调用 System.currentTimeMillis
。
出于测试目的,现在我们只能手动更新系统时钟来操纵代码认为测试正在运行的时间和日期。
所以我的问题是:
有没有办法覆盖 System.currentTimeMillis 返回的内容?例如,告诉 JVM 在从该方法返回之前自动添加或减去一些偏移量?
Is there a way, either in code or with JVM arguments, to override the current time, as presented via System.currentTimeMillis
, other than manually changing the system clock on the host machine?
A little background:
We have a system that runs a number of accounting jobs that revolve much of their logic around the current date (ie 1st of the month, 1st of the year, etc)
Unfortunately, a lot of the legacy code calls functions such as new Date()
or Calendar.getInstance()
, both of which eventually call down to System.currentTimeMillis
.
For testing purposes, right now, we are stuck with manually updating the system clock to manipulate what time and date the code thinks that the test is being run.
So my question is:
Is there a way to override what is returned by System.currentTimeMillis
? For example, to tell the JVM to automatically add or subtract some offset before returning from that method?
发布评论
评论(12)
我强烈建议您不要弄乱系统时钟,而是硬着头皮重构遗留代码以使用可替换的时钟。 理想这应该通过依赖注入来完成,但即使您使用可替换的单例,您也将获得可测试性。
这几乎可以通过搜索和替换单例版本来实现自动化:
Calendar.getInstance()
替换为Clock.getInstance().getCalendarInstance()
。new Date()
替换为Clock.getInstance().newDate()
System.currentTimeMillis()
替换为Clock.getInstance( ).currentTimeMillis()
(根据需要等)
一旦迈出了第一步,您就可以用 DI 一次一点地替换单例。
I strongly recommend that instead of messing with the system clock, you bite the bullet and refactor that legacy code to use a replaceable clock. Ideally that should be done with dependency injection, but even if you used a replaceable singleton you would gain testability.
This could almost be automated with search and replace for the singleton version:
Calendar.getInstance()
withClock.getInstance().getCalendarInstance()
.new Date()
withClock.getInstance().newDate()
System.currentTimeMillis()
withClock.getInstance().currentTimeMillis()
(etc as required)
Once you've taken that first step, you can replace the singleton with DI a bit at a time.
太长了;博士
是的。
java.time 中的
Clock
我们针对可插拔时钟替换问题提供了一个新的解决方案,以方便使用假日期时间值进行测试。 java.time 包 Java 8 包含一个抽象类
java.time.Clock
,其中明确的目的:您可以插入您自己的
Clock
实现,尽管您可能会找到一个已经实现的时钟来满足您的需求。为了您的方便,java.time 包含静态方法来产生特殊的实现。这些替代实现在测试期间可能很有价值。改变节奏
各种
tick...
方法产生以不同节奏增加当前时刻的时钟。默认
时钟
报告时间更新频率为毫秒在 Java 8 和 Java 9 中与 纳秒 一样精细(取决于您的硬件)。您可以要求以不同的粒度报告当前的真实时刻。tickSeconds
- 以整秒为单位递增tickMinutes
- 整分钟增量tick
- 按传递的Duration
参数。虚假时钟
有些时钟可能会撒谎,产生与主机操作系统硬件时钟不同的结果。
fixed
- 将单个不变(非增量)时刻报告为当前时刻。offset
- 报告当前时刻,但按传递的持续时间
参数。比如锁定今年最早圣诞节的第一时刻。换句话说,当圣诞老人和他的驯鹿到达第一站时。现在最早的时区似乎是
Pacific/Kiritimati
+14:00
。使用那个特殊的固定时钟总是返回同一时刻。我们在 Kiritimati 迎来了圣诞节的第一个时刻,UTC 显示挂钟时间 提前 14 个小时,即 12 月 24 日前一天上午 10 点。
请参阅 IdeOne 中的实时代码。 com。
真实时间,不同时区
您可以通过
Clock
实现来控制分配哪个时区。这在某些测试中可能有用。但我不建议在生产代码中这样做,您应该始终显式指定可选的ZoneId
或ZoneOffset
参数。您可以指定 UTC 作为默认区域。
您可以指定任何特定时区。以
大陆/地区
格式指定正确的时区名称 ,例如America/Montreal
、非洲/卡萨布兰卡
,或太平洋/奥克兰
。切勿使用 3-4 个字母的缩写,例如EST
或IST
,因为它们不是真正的时区,不是标准化的,甚至不是唯一的( !)。您可以指定 JVM 当前的默认时区应该是特定
Clock
对象的默认时区。运行此代码进行比较。请注意,它们都报告同一时刻、时间线上的同一点。它们仅在挂钟时间方面有所不同;换句话说,用三种方式表达同一件事,用三种方式展示同一时刻。
America/Los_Angeles
是运行此代码的计算机上的 JVM 当前默认区域。Instant
类根据定义始终采用 UTC。因此,这三个与区域相关的Clock
用法具有完全相同的效果。默认时钟
Instant.now
默认使用的实现是Clock.systemUTC()
。这是当您不指定时钟
时使用的实现。亲自查看Instant.now
的预发布 Java 9 源代码。OffsetDateTime.now
和ZonedDateTime.now
的默认Clock
是Clock.systemDefaultZone()
。请参阅 源代码。默认实现的行为在 Java 8 和 Java 9 之间发生了变化。在 Java 8 中,当前时刻仅通过 毫秒,尽管类能够存储纳秒的分辨率。 Java 9 带来了一种新的实现,能够以纳秒的分辨率捕获当前时刻——当然,这取决于计算机硬件时钟的功能。
关于 java.time
java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧遗留日期时间类,例如
java.util.Date
,日历
, &SimpleDateFormat
。要了解更多信息,请参阅 Oracle 教程。并在 Stack Overflow 上搜索许多示例和解释。规范为 JSR 310。
Joda-Time 项目,现已在 维护模式,建议迁移到 java.time 类。
您可以直接与数据库交换java.time对象。使用符合 JDBC 驱动程序 /jeps/170" rel="nofollow noreferrer">JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* 类。 Hibernate 5 和 Hibernate 5 JPA 2.2 支持 java.time。
从哪里获取 java.time 类?
tl;dr
Yes.
Clock
In java.timeWe have a new solution to the problem of a pluggable clock replacement to facilitate testing with faux date-time values. The java.time package in Java 8 includes an abstract class
java.time.Clock
, with an explicit purpose:You could plug in your own implementation of
Clock
, though you likely can find one already made to meet your needs. For your convenience, java.time includes static methods to yield special implementations. These alternate implementations can be valuable during testing.Altered cadence
The various
tick…
methods produce clocks that increment the current moment with a different cadence.The default
Clock
reports a time updated as frequently as milliseconds in Java 8 and in Java 9 as fine as nanoseconds (depending on your hardware). You can ask for the true current moment to be reported with a different granularity.tickSeconds
- Increments in whole secondstickMinutes
- Increments in whole minutestick
- Increments by the passedDuration
argument.False clocks
Some clocks can lie, producing a result different than that of the host OS’ hardware clock.
fixed
- Reports a single unchanging (non-incrementing) moment as the current moment.offset
- Reports the current moment but shifted by the passedDuration
argument.For example, lock in the first moment of the earliest Christmas this year. in other words, when Santa and his reindeer make their first stop. The earliest time zone nowadays seems to be
Pacific/Kiritimati
at+14:00
.Use that special fixed clock to always return the same moment. We get the first moment of Christmas day in Kiritimati, with UTC showing a wall-clock time of fourteen hours earlier, 10 AM on the prior date of the 24th of December.
See live code in IdeOne.com.
True time, different time zone
You can control which time zone is assigned by the
Clock
implementation. This might be useful in some testing. But I do not recommend this in production code, where you should always specify explicitly the optionalZoneId
orZoneOffset
arguments.You can specify that UTC be the default zone.
You can specify any particular time zone. Specify a proper time zone name in the format of
continent/region
, such asAmerica/Montreal
,Africa/Casablanca
, orPacific/Auckland
. Never use the 3-4 letter abbreviation such asEST
orIST
as they are not true time zones, not standardized, and not even unique(!).You can specify the JVM’s current default time zone should be the default for a particular
Clock
object.Run this code to compare. Note that they all report the same moment, the same point on the timeline. They differ only in wall-clock time; in other words, three ways to say the same thing, three ways to display the same moment.
America/Los_Angeles
was the JVM current default zone on the computer that ran this code.The
Instant
class is always in UTC by definition. So these three zone-relatedClock
usages have exactly the same effect.Default clock
The implementation used by default for
Instant.now
is the one returned byClock.systemUTC()
. This is the implementation used when you do not specify aClock
. See for yourself in pre-release Java 9 source code forInstant.now
.The default
Clock
forOffsetDateTime.now
andZonedDateTime.now
isClock.systemDefaultZone()
. See source code.The behavior of the default implementations changed between Java 8 and Java 9. In Java 8, the current moment is captured with a resolution only in milliseconds despite the classes’ ability to store a resolution of nanoseconds. Java 9 brings a new implementation able to capture the current moment with a resolution of nanoseconds – depending, of course, on the capability of your computer hardware clock.
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as
java.util.Date
,Calendar
, &SimpleDateFormat
.To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for
java.sql.*
classes. Hibernate 5 & JPA 2.2 support java.time.Where to obtain the java.time classes?
正如 Jon Skeet 所说的 :
所以这里(假设您刚刚将所有
new Date()
替换为new DateTime().toDate()
)如果您想导入具有接口的库(请参阅下面乔恩的评论),您可以使用 Prevayler's Clock,将提供实现以及标准接口。完整的 jar 只有 96kB,所以它不应该花很多钱......
As said by Jon Skeet:
So here goes (presuming you've just replaced all your
new Date()
withnew DateTime().toDate()
)If you want import a library that has an interface (see Jon's comment below), you could just use Prevayler's Clock, which will provide implementations as well as the standard interface. The full jar is only 96kB, so it shouldn't break the bank...
虽然使用某些 DateFactory 模式看起来不错,但它并不涵盖您无法控制的库 - 想象一下验证注释 @Past 以及依赖于 System.currentTimeMillis 的实现(有这样的)。
这就是为什么我们使用 jmockit 直接模拟系统时间:
因为不可能获得 millis 的原始未模拟值,所以我们使用 Nano 计时器代替 - 这与挂钟无关,但相对时间就足够了:
有记录的问题,使用 HotSpot,多次调用后时间会恢复正常 - 这是问题报告: http://code.google.com/p/jmockit/issues/detail?id=43
为了克服这个问题,我们必须打开一个特定的 HotSpot 优化 - 使用此参数运行 JVM
-XX:-内联
。虽然这对于生产来说可能并不完美,但它对于测试来说已经足够了,并且对于应用程序来说绝对透明,特别是当 DataFactory 没有商业意义并且只是因为测试而引入时。如果有内置的 JVM 选项在不同的时间运行就好了,可惜没有这样的 hacks 是不可能的。
完整的故事在我的博客文章中:
http://virgo47.wordpress.com/2012/06/22/chang -system-time-in-java/
帖子中提供了完整方便的类 SystemTimeShifter。类可以在您的测试中使用,也可以非常轻松地用作真正的主类之前的第一个主类,以便在不同的时间运行您的应用程序(甚至整个应用程序服务器)。当然,这主要是为了测试目的,而不是用于生产环境。
2014 年 7 月编辑:JMockit 最近发生了很大变化,您必须使用 JMockit 1.0 才能正确使用它(IIRC)。绝对无法升级到界面完全不同的最新版本。我正在考虑只内联必要的东西,但由于我们在新项目中不需要这个东西,所以我根本不开发这个东西。
While using some DateFactory pattern seems nice, it does not cover libraries you can't control - imagine Validation annotation @Past with implementation relying on System.currentTimeMillis (there is such).
That's why we use jmockit to mock the system time directly:
Because it's not possible to get to the original unmocked value of millis, we use nano timer instead - this is not related to wall clock, but relative time suffices here:
There is documented problem, that with HotSpot the time gets back to normal after a number of calls - here is the issue report: http://code.google.com/p/jmockit/issues/detail?id=43
To overcome this we have to turn on one specific HotSpot optimization - run JVM with this argument
-XX:-Inline
.While this may not be perfect for production, it is just fine for tests and it is absolutely transparent for application, especially when DataFactory doesn't make business sense and is introduced only because of tests. It would be nice to have built-in JVM option to run in different time, too bad it is not possible without hacks like this.
Complete story is in my blog post here:
http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/
Complete handy class SystemTimeShifter is provided in the post. Class can be used in your tests, or it can be used as the first main class before your real main class very easily in order to run your application (or even whole appserver) in a different time. Of course, this is intented for testing purposes mainly, not for production environment.
EDIT July 2014: JMockit changed a lot lately and you are bound to use JMockit 1.0 to use this correctly (IIRC). Definitely can't upgrade to newest version where interface is completly different. I was thinking about inlining just the necessary stuff, but as we don't need this thing in our new projects I'm not developing this thing at all.
Powermock 效果很好。只是用它来模拟 System.currentTimeMillis() 。
Powermock works great. Just used it to mock
System.currentTimeMillis()
.使用面向方面的编程(AOP,例如 AspectJ)来编织 System 类以返回可以在测试用例中设置的预定义值。
或者编织应用程序类以将对 System.currentTimeMillis() 或 new Date() 的调用重定向到您自己的另一个实用程序类。
然而,编织系统类 (
java.lang.*
) 有点棘手,您可能需要对 rt.jar 执行离线编织,并使用单独的 JDK/rt.jar 进行测试。它称为二进制编织,还有特殊的工具 执行系统类的编织并规避一些问题(例如引导虚拟机可能不起作用)
Use Aspect-Oriented Programming (AOP, for example AspectJ) to weave the System class to return a predefined value which you could set within your test cases.
Or weave the application classes to redirect the call to
System.currentTimeMillis()
or tonew Date()
to another utility class of your own.Weaving system classes (
java.lang.*
) is however a little bit more trickier and you might need to perform offline weaving for rt.jar and use a separate JDK/rt.jar for your tests.It's called Binary weaving and there are also special tools to perform weaving of System classes and circumvent some problems with that (e.g. bootstrapping the VM may not work)
一种在 Java 8 Web 应用程序中使用 EasyMock 覆盖当前系统时间以进行 JUnit 测试的工作方法,无需 Joda Time 和 PowerMock。
以下是您需要做的事情:
在被测试的类中需要做的事情
步骤 1
向被测试的类
MyService
添加一个新的java.time.Clock
属性,并确保新属性将使用实例化块或构造函数以默认值正确初始化:步骤 2
将新属性
clock
注入到调用当前日期时间的方法中。例如,在我的例子中,我必须检查 dataase 中存储的日期是否发生在LocalDateTime.now()
之前,我将其替换为LocalDateTime.now(clock)
>,如下所示:测试类中需要做什么
步骤 3
在测试类中,创建一个模拟时钟对象,并在调用测试方法
doExecute()
,然后立即将其重置回来,如下所示:在调试模式下检查它,您将看到 2017 年 2 月 3 日的日期已正确注入到
myService
实例中并用于比较指令,然后已使用initDefaultClock()
正确重置为当前日期。A working way to override current system time for JUnit testing purposes in a Java 8 web application with EasyMock, without Joda Time, and without PowerMock.
Here's what you need to do:
What needs to be done in the tested class
Step 1
Add a new
java.time.Clock
attribute to the tested classMyService
and make sure the new attribute will be initialized properly at default values with an instantiation block or a constructor:Step 2
Inject the new attribute
clock
into the method which calls for a current date-time. For instance, in my case I had to perform a check of whether a date stored in dataase happened beforeLocalDateTime.now()
, which I remplaced withLocalDateTime.now(clock)
, like so:What needs to be done in the test class
Step 3
In the test class, create a mock clock object and inject it into the tested class's instance just before you call the tested method
doExecute()
, then reset it back right afterwards, like so:Check it in debug mode and you will see the date of 2017 Feb 3 has been correctly injected into
myService
instance and used in the comparison instruction, and then has been properly reset to current date withinitDefaultClock()
.在我看来,只有非侵入性的解决方案才能发挥作用。特别是如果您有外部库和大型遗留代码库,则没有可靠的方法来模拟时间。
JMockit ...仅适用于有限数量的 次
Co ...需要将客户端模拟为System.currentTimeMillis()。又是一种侵入性的选择。
由此我只看到提到的 javaagent 或 aop 方法对整个系统是透明的。有人这样做过并且可以指出这样的解决方案吗?
@jarnbjo:您能展示一些 javaagent 代码吗?
In my opinion only a none-invasive solution can work. Especially if you have external libs and a big legacy code base there is no reliable way to mock out time.
JMockit ... works only for restricted number of times
PowerMock & Co ...needs to mock the clients to System.currentTimeMillis(). Again an invasive option.
From this I only see the mentioned javaagent or aop approach being transparent to the whole system. Has anybody done that and could point to such a solution?
@jarnbjo: could you show some of the javaagent code please?
这是使用 PowerMockito 的示例。还有一个 new Date() 的示例。
有关模拟系统类的更多详细信息。
您正在测试的一些遗留类:
控制台输出
Here is an example using PowerMockito. Also there is an example with new Date().
More details about mocking system classes.
Some legacy class you are testing:
Console output
实际上没有办法直接在虚拟机中执行此操作,但您可以通过编程方式在测试计算机上设置系统时间。大多数(全部?)操作系统都有命令行命令来执行此操作。
There really isn't a way to do this directly in the VM, but you could all something to programmatically set the system time on the test machine. Most (all?) OS have command line commands to do this.
如果您运行的是 Linux,则可以使用 libfaketime 的 master 分支,或者在测试提交时 4ce2835。
只需使用您想要模拟 Java 应用程序的时间设置环境变量,然后使用 ld-preloading 运行它:
第二个环境变量对于 Java 应用程序至关重要,否则会冻结。在撰写本文时,它需要 libfaketime 的 master 分支。
如果您想更改 systemd 托管服务的时间,只需将以下内容添加到您的单元文件覆盖中,例如对于elasticsearch,这将是
/etc/systemd/system/elasticsearch.service.d/override.conf
:不要忘记使用 `systemctl daemon-reload 重新加载 systemd
If you're running Linux, you can use the master branch of libfaketime, or at the time of testing commit 4ce2835.
Simply set the environment variable with the time you'd like to mock your java application with, and run it using ld-preloading:
The second environment variable is paramount for java applications, which otherwise would freeze. It requires the master branch of libfaketime at the time of writing.
If you'd like to change the time of a systemd managed service, just add the following to your unit file overrides, e.g. for elasticsearch this would be
/etc/systemd/system/elasticsearch.service.d/override.conf
:Don't forget to reload systemd using `systemctl daemon-reload
如果您想模拟具有
System.currentTimeMillis()
参数的方法,那么您可以传递anyLong()
的 Matchers 类作为参数。PS 我能够使用上述技巧成功运行我的测试用例,只是为了分享有关我使用 PowerMock 和 Mockito 框架的测试的更多详细信息。
If you want to mock the method having
System.currentTimeMillis()
argument then you can passanyLong()
of Matchers class as an argument.P.S. I am able to run my test case successfully using the above trick and just to share more details about my test that I am using PowerMock and Mockito frameworks.