有没有办法,无论是在代码中还是在JVM参数中,都可以通过System.currentTimeMillis
覆盖当前时间,而不是手动更改主机上的系统时钟?
一点背景:
我们有一个系统可以运行许多会计工作,这些工作围绕当前日期(即本月的第一天,一年中的第一天等)展开大部分逻辑。
不幸的是,许多遗留代码会调用new Date()
或Calendar.getInstance()
等函数,这两种函数最终都会调用System.currentTimeMillis
。
出于测试目的,我们现在仍然无法手动更新系统时钟来操纵代码认为正在运行测试的时间和日期。
所以我的问题是:
有没有办法覆盖System.currentTimeMillis
返回的内容?例如,告诉JVM在从该方法返回之前自动添加或减去一些偏移量?
提前致谢!
答案 0 :(得分:102)
我强烈建议您不要乱用系统时钟,而是扼杀子弹并重构旧代码以使用可更换的时钟。 理想情况应该通过依赖注入来完成,但即使您使用了可替换的单例,也可以获得可测试性。
这可以通过搜索和替换单身版本来实现自动化:
Calendar.getInstance()
替换为Clock.getInstance().getCalendarInstance()
。new Date()
替换为Clock.getInstance().newDate()
System.currentTimeMillis()
替换为Clock.getInstance().currentTimeMillis()
(根据需要等)
一旦你完成了第一步,你可以一次用DI替换单身。
答案 1 :(得分:58)
有没有办法,无论是在代码中还是在JVM参数中,都可以覆盖当前时间,如System.currentTimeMillis所示,而不是手动更改主机上的系统时钟?
是
Instant.now(
Clock.fixed(
Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
)
)
Clock
在java.time 我们有一个新的解决方案来解决可插拔时钟更换的问题,以便于使用 faux 日期时间值进行测试。 java.time package中的Java 8包含一个抽象类java.time.Clock
,其目的明确:
允许在需要时插入备用时钟
您可以插入自己的Clock
实现,但您可能会找到一个已满足您需求的实现。为方便起见,java.time包含静态方法以产生特殊实现。这些替代实现在测试期间可能是有价值的
各种tick…
方法产生的时钟以不同的节奏增加当前时刻。
默认Clock
报告的时间频率与Java 8和Java 9中的milliseconds一样频繁,更新为nanoseconds(具体取决于您的硬件)。您可以要求以不同的粒度报告真实的当前时刻。
tickSeconds
- 整秒的增量tickMinutes
- 整分钟的增量tick
- 通过Duration
参数的增量。某些时钟可能存在,产生的结果与主机操作系统硬件时钟的结果不同。
例如,锁定今年最早圣诞节的第一时刻。换句话说,当Santa and his reindeer make their first stop时。现在最早的时区似乎是+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 );
。
Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );
使用该特殊固定时钟始终返回相同的时刻。我们在Pacific/Kiritimati
获得了圣诞节的第一个时刻,其中UTC显示了提前12个小时的Kiritimati,即12月24日的上午10点。
Clock
instant.toString():2016-12-24T10:00:00Z
zdt.toString():2016-12-25T00:00 + 14:00 [Pacific / Kiritimati]
请参阅wall-clock time。
您可以控制ZoneId
实施分配的时区。这在某些测试中可能很有用。但我不建议在生产代码中使用此代码,您应该始终明确指定可选的ZoneOffset
或ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );
参数。
您可以指定UTC是默认区域。
continent/region
您可以指定任何特定时区。以Pacific/Auckland
格式指定live code in IdeOne.com,例如proper time zone name,America/Montreal
或EST
。切勿使用诸如IST
或ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
之类的3-4字母缩写,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。
Clock
您可以指定JVM的当前默认时区应该是特定ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );
对象的默认时区。
System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );
运行此代码进行比较。请注意,它们都报告了相同的时刻,即时间轴上的相同点。它们的区别仅在于Africa/Casablanca
;换句话说,三种说法相同的方式,三种方式来表现同一时刻。
America/Los_Angeles
Clock
是运行此代码的计算机上的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]
根据定义,wall-clock time类始终为UTC。因此,这三个与区域相关的Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );
用法具有完全相同的效果。
Instant.now
instantClockSystemUTC.toString():2016-12-31T20:52:39.763Z
instantClockSystem.toString():2016-12-31T20:52:39.763Z
instantClockSystemDefaultZone.toString():2016-12-31T20:52:39.763Z
Clock
默认使用的实现是Instant
返回的实现。这是您未指定public static Instant now() {
return Clock.systemUTC().instant();
}
时使用的实现。请参阅Clock.systemUTC()
。
Clock
OffsetDateTime.now
和ZonedDateTime.now
的默认Clock.systemDefaultZone()
为public static ZonedDateTime now() {
return now(Clock.systemDefaultZone());
}
。请参阅pre-release Java 9 source code for Instant.now
。
java.sql.*
默认实现的行为在Java 8和Java 9之间发生了变化。在Java 8中,尽管类具有存储{{3}的分辨率的能力,但仅在source code中使用分辨率捕获当前时刻。 }。 Java 9带来了一种新的实现方式,能够以纳秒的分辨率捕获当前时刻 - 当然,这取决于计算机硬件时钟的能力。
milliseconds框架内置于Java 8及更高版本中。这些类取代了麻烦的旧nanoseconds日期时间类,例如java.time,legacy和& java.util.Date
现在位于Calendar
的SimpleDateFormat
项目建议迁移到Joda-Time类。
要了解详情,请参阅maintenance mode。并搜索Stack Overflow以获取许多示例和解释。规范是java.time。
您可以直接与数据库交换 java.time 对象。使用符合Oracle Tutorial或更高版本的JSR 310。不需要字符串,不需要{{1}}类。
从哪里获取java.time类?
ThreeTenABP项目使用其他类扩展java.time。该项目是未来可能添加到java.time的试验场。您可以在此处找到一些有用的课程,例如How to use ThreeTenABP…,ThreeTen-Extra,Interval
和YearWeek
。
答案 2 :(得分:41)
“使用Joda Time”几乎总是最好的答案,涉及“如何使用java.util.Date/Calendar实现X?”
所以这里(假设您刚刚用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's Clock,它将提供实现以及标准接口。完整的罐子只有96kB,所以它不应该破坏银行...
答案 3 :(得分:15)
虽然使用一些DateFactory模式似乎很好,但它不包括你无法控制的库 - 想象验证注释@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);
因为无法达到millis的原始未锁定值,我们使用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;
}
有记录的问题,使用HotSpot,经过多次调用后,时间恢复正常 - 这是问题报告:http://code.google.com/p/jmockit/issues/detail?id=43
为了解决这个问题,我们必须启用一个特定的HotSpot优化 - 使用此参数-XX:-Inline
运行JVM。
虽然这对于生产来说可能并不完美,但对于测试来说它是完美的,它对于应用来说绝对透明,特别是当DataFactory没有商业意义并且仅仅是因为测试而引入时。有内置的JVM选项在不同的时间运行会很不错,太糟糕了,没有这样的黑客就不可能。
完整的故事在我的博客文章中: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/
帖子中提供了完整的便捷类SystemTimeShifter。类可以在您的测试中使用,或者它可以非常容易地用作真实主类之前的第一个主类,以便在不同的时间运行您的应用程序(甚至整个应用程序服务器)。当然,这主要是用于测试目的,而不是用于生产环境。
2014年7月编辑:JMockit最近发生了很大的变化,你必然会使用JMockit 1.0来正确使用它(IIRC)。绝对无法升级到界面完全不同的最新版本。我正在考虑内联必要的东西,但是因为我们在新的项目中不需要这个东西,所以我根本就没有开发这个东西。
答案 4 :(得分:7)
Powermock效果很好。只是用它来模仿System.currentTimeMillis()
。
答案 5 :(得分:5)
使用面向方面编程(AOP,例如AspectJ)编织System类,以返回您可以在测试用例中设置的预定义值。
或者编织应用程序类,将调用重定向到System.currentTimeMillis()
或new Date()
到另一个自己的实用程序类。
编织系统类(java.lang.*
)有点棘手,您可能需要为rt.jar执行离线编织,并为测试使用单独的JDK / rt.jar。
它被称为二进制编织,并且还有特殊的tools来执行系统类的编织并避免一些问题(例如,引导VM可能不起作用)
答案 6 :(得分:3)
实际上没有办法在VM中直接执行此操作,但您可以通过编程方式在测试计算机上设置系统时间。大多数(全部?)操作系统都有命令行命令来执行此操作。
答案 7 :(得分:3)
以下是您需要做的事情:
向测试类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
}
// (...)
}
将新属性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();
}
}
// (...)
}
在测试类中,创建一个模拟时钟对象,并在调用测试方法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年2月3日的日期已正确注入myService
实例并在比较说明中使用,然后已使用{{1}正确重置为当前日期}。
答案 8 :(得分:2)
在我看来,只有非侵入式解决方案才有效。特别是如果你有外部库和一个很大的遗留代码库,就没有可靠的方法来模拟时间。
JMockit ...仅适用于受限制的times
PowerMock& Co ...需要模拟clients到System.currentTimeMillis()。再次成为一种侵入性选择。
从这里我只看到提到的 javaagent 或 aop 方法对整个系统是透明的。有没有人这样做,可以指出这样的解决方案?
@jarnbjo:你能展示一些javaagent代码吗?
答案 9 :(得分:1)
如果您正在运行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的主分支。
如果您想更改systemd托管服务的时间,只需将以下内容添加到您的单元文件覆盖中,例如:对于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 daemon-reload
重新加载systemd答案 10 :(得分:0)
这是一个使用 PowerMockito 的示例。还有一个 new Date() 的例子。
有关 mocking system classes 的更多详细信息。
@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
答案 11 :(得分:-2)
如果要模拟具有System.currentTimeMillis()
参数的方法,则可以将anyLong()
Matchers类作为参数传递。
P.S。我可以使用上面的技巧成功运行我的测试用例,只是为了分享我使用PowerMock和Mockito框架的测试的更多细节。