重写 Java System.currentTimeMillis 以测试对时间敏感的代码

除了手动更改主机上的系统时钟之外,是否还有其他方法,无论是在代码中还是使用 JVM 参数,来覆盖当前时间,如通过 System.currentTimeMillis所显示的那样?

一个小小的背景:

我们有一个系统,运行许多会计工作,围绕当前日期(即1月1日,1月1日,等)的逻辑大部分

遗憾的是,许多遗留代码调用诸如 new Date()Calendar.getInstance()之类的函数,这两个函数最终都会调用到 System.currentTimeMillis

出于测试的目的,现在,我们必须手动更新系统时钟来操作代码认为正在运行测试的时间和日期。

所以我的问题是:

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

121582 次浏览

实际上没有一种方法可以直接在 VM 中完成这项工作,但是您可以通过编程方式在测试机器上设置系统时间。大部分(全部?)操作系统有命令行命令来执行此操作。

很强烈建议,与其扰乱系统时钟,不如咬紧牙关重构遗留代码,使用可替换的时钟。但是即使你使用一个可替换的单例模式,你也可以获得测试依赖注入。

这几乎可以通过搜索和替换单例版本实现自动化:

  • Clock.getInstance().getCalendarInstance()代替 Calendar.getInstance()
  • Clock.getInstance().newDate()代替 new Date()
  • Clock.getInstance().currentTimeMillis()代替 System.currentTimeMillis()

(视需要等)

一旦您完成了第一步,您就可以一次用 DI 替换一点单例。

作为 乔恩 · 斯基特说:

对于任何涉及到“如何使用 java.util 实现 X”的问题,“使用 Joda Time”几乎总是最好的答案。日期/日历?”

现在开始(假设你已经用 new DateTime().toDate()代替了所有的 new Date())

//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 在下面的评论) ,您可以只使用 普雷维勒之钟,它将提供实现以及标准接口。整个罐子只有96kB,所以它不应该打破银行..。

使用面向侧面的程序设计(AOP,例如 AspectJ)编织 System 类来返回一个预定义的值,您可以在测试用例中设置这个值。

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

然而编织系统类(java.lang.*)要稍微复杂一些,您可能需要为 rt.jar 执行离线编织,并在测试中使用单独的 JDK/rt.jar。

它被称为 二进制编织,并且还有特殊的 工具来执行系统类的编织,并且用它来规避一些问题(例如,引导虚拟机可能无法工作)

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

在我看来,只有非侵入性的解决方案才能奏效。特别是如果您有外部库和一个大的遗留代码库,就没有可靠的方法来模拟时间。

JMockit... 只适用于限制数量的 时间

PowerMock & Co... 需要将 客户模拟为 System.currentTimeMillis ()。

从这里我只看到提到的 JavaagentAop方法对整个系统是透明的。有人这样做过吗? 有人能指出这样一种解决方案吗?

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

虽然使用一些 DateFactory 模式看起来不错,但是它并没有涵盖你无法控制的库——想象一下依赖 System.currentTimeMillis 实现的验证注释@Past (有这样的实现)。

这就是为什么我们使用 jmocit 直接模拟系统时间:

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);

因为不可能得到毫米的原始未模拟值,所以我们改用 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

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

虽然这对于生产来说可能不是完美的,但是对于测试来说却是很好的,并且对于应用程序来说是绝对透明的,特别是当 DataFactory 没有业务意义并且仅仅是因为测试而引入的时候。如果有内置的 JVM 选项可以在不同的时间运行就好了,但是如果没有这样的技巧就不可能了。

完整的故事发表在我的博客上: Http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

本文提供了完整的方便类 SystemTimeShiffer。类可以在测试中使用,也可以非常容易地将它用作实际主类之前的第一个主类,以便在不同的时间运行应用程序(甚至整个 appserver)。当然,这主要是为了测试目的,而不是为了生产环境。

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

博士

除了手动更改主机上的系统时钟之外,是否还有其他方法,无论是在代码中还是使用 JVM 参数,来覆盖当前时间,如 System.currentTimeMillis 所示?

是的。

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

时间 Clock

我们有一个新的解决方案的问题,可插式时钟更换,以促进测试与 人造的日期时间值。爪哇8中的 Java.time 软件包包括一个抽象类 java.time.Clock,其目的很明确:

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

您可以插入自己的 Clock实现,尽管您可能会找到一个已经满足您需要的实现。为了方便起见,java.time 包含静态方法以产生特殊的实现。这些替代实现在测试期间可能很有价值。

改变了节奏

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

默认的 Clock报告的时间更新频率与 Java8中的 毫秒和 Java9中的 纳秒一样(取决于您的硬件)。您可以要求以不同的粒度报告真实的当前时刻。

假钟

有些时钟会撒谎,产生的结果与主机操作系统的硬件时钟不同。

  • fixed -将一个不变(非增量)矩报告为当前矩。
  • offset -报告当前时刻,但被传递的 Duration参数移位。

例如,锁定今年最早的圣诞节的第一个时刻。换句话说,当 圣诞老人和他的驯鹿到达了他们的第一站。现在最早的时区似乎是 +14:00Pacific/Kiritimati时区。

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 );

使用那个特殊的固定时钟总是返回相同的时刻。我们得到的第一个时刻的圣诞节一天在世界协调时显示的 abc1的14小时前,上午10时的前一天的12月24日。

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

ToString () : 2016-12-24T10:00:00Z

ToString () : 2016-12-25 t00:00 + 14:00[ Pacific/Kiritimati ]

参见 在 IdeOne.com 上直播代码

真正的时间,不同的时区

您可以控制 Clock实现分配的时区。这可能在某些测试中有用。但是我不建议在生产代码中使用这种方法,因为在生产代码中应该始终显式地指定可选的 ZoneIdZoneOffset参数。

可以指定 UTC 为默认区域。

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

您可以指定任何特定的时区。指定 continent/region格式的 正确的时区名称,例如 America/MontrealAfrica/CasablancaPacific/Auckland。永远不要使用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 当前默认区域。

ToString () : 2016-12-31T20:52:39.688 Z

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 () );

ToString () : 2016-12-31T20:52:39.763 Z

InstClockSystem.toString () : 2016-12-31T20:52:39.763 Z

InstClockSystemDefaultZone.toString () : 2016-12-31T20:52:39.763 Z

默认时钟

Instant.now默认使用的实现是 Clock.systemUTC()返回的实现。这是在不指定 Clock时使用的实现。在 Instant.now的预发布 Java9源代码中自己看。

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

OffsetDateTime.nowZonedDateTime.now的默认 ClockClock.systemDefaultZone()

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

默认实现的行为在 Java8和 Java9之间发生了变化。在 Java8中,尽管类能够存储 纳秒的分辨率,但是只能用 毫秒中的分辨率捕获当前时刻。Java9带来了一个新的实现,它能够以纳秒级的分辨率捕捉当前时刻——当然,这取决于计算机硬件时钟的性能。


关于 爪哇时间

< em > java.time 框架内置于 Java8及更高版本中。这些类取代了令人讨厌的旧的 遗产日期时间类,如 java.util.DateCalendarSimpleDateFormat

要了解更多,请参阅 < em > Oracle 教程 。并搜索堆栈溢出许多例子和解释。规范是 JSR 310

现在在 维修模式中的 尤达时间项目建议迁移到 爪哇时间类。

您可以直接与数据库交换 爪哇时间对象。使用与 JDBC 4.2或更高版本兼容的 JDBC 驱动程序。不需要字符串,不需要 java.sql.*类。Hibernate 5 & JPA 2.2支持 爪哇时间

从哪里获得 java.time 类?

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

附注: 我能够使用上述技巧成功地运行我的测试用例,并且只是为了分享更多关于我使用 PowerMock 和 Mockito 框架的测试的细节。

在 Java8 Web 应用程序中,用 EasyMock 覆盖当前系统时间的一种工作方法,不使用 Joda Time,也不使用 PowerMock。

你需要做的是:

在测试类中需要做的事情

第一步

向测试的类 MyService添加一个新的 java.time.Clock属性,并确保新属性在默认值下用实例化块或构造函数正确初始化:

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实例,并在比较指令中使用,然后已被正确地重置为当前日期与 initDefaultClock()

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

只需设置模拟 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

第二个环境变量对于 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 重新加载 system

下面是一个使用 PowerMockito 的例子,还有一个使用 新日期()的例子。
有关 模拟系统类的详情。

@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