覆盖 Java System.currentTimeMillis 以测试时间敏感代码

新手上路,请多包涵

除了手动更改主机上的系统时钟之外,有没有办法在代码中或使用 JVM 参数覆盖当前时间,如通过 System.currentTimeMillis

一点背景:

我们有一个运行许多会计工作的系统,这些工作的大部分逻辑都围绕当前日期(即每月的第一天、今年的第一天等)

不幸的是,许多遗留代码调用函数,例如 new Date()Calendar.getInstance() ,它们最终都调用到 System.currentTimeMillis

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

所以我的问题是:

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

原文由 Mike Clark 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 907
2 个回答

强烈 建议您不要弄乱系统时钟,而是硬着头皮重构遗留代码以使用可替换时钟。 _理想情况下_,这应该通过依赖注入来完成,但即使您使用可替换的单例,您也会获得可测试性。

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

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

(根据需要等)

迈出第一步后,您可以一次用 DI 替换单例。

原文由 Jon Skeet 发布,翻译遵循 CC BY-SA 2.5 许可协议

tl;博士

除了手动更改主机上的系统时钟之外,是否有一种方法可以在代码中或使用 JVM 参数覆盖当前时间(如 System.currentTimeMillis 所示)?

是的。

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

Clock 在 java.time 中

我们有一个新的解决方案来解决可插入时钟替换问题,以方便使用 虚假 日期时间值进行测试。 Java 8 中的 java.time 包 包含一个抽象类 java.time.Clock ,具有明确的目的:

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

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

改变节奏

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

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

假钟

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

  • fixed - 将单个不变(非递增)时刻报告为当前时刻。
  • offset - 报告当前时刻,但由传递的 Duration 参数移动。

比如锁定今年最早的圣诞节第一时刻。换句话说,当 圣诞老人和他的驯鹿第一次停下来 时。现在最早的时区似乎是 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 获得圣诞节的第一刻,UTC 显示 挂钟时间 提前 14 小时,即 12 月 24 日之前的上午 10 点。

 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 实现分配的时区。这在某些测试中可能很有用。但我不建议在生产代码中这样做,您应该始终明确指定可选的 ZoneIdZoneOffset 参数。

您可以将 UTC 指定为默认时区。

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

您可以指定任何特定时区。 Specify a proper time zone name in the format of continent/region , such as America/Montreal , Africa/Casablanca , or Pacific/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 当前默认区域。

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[美国/洛杉矶]

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 时使用的实现。在 Instant.now 的预发布 Java 9 源代码中亲自查看

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

默认 Clock 对于 OffsetDateTime.nowZonedDateTime.nowClock.systemDefaultZone() 请参阅 源代码

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

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


关于 java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

要了解更多信息,请参阅 Oracle 教程。并在 Stack Overflow 中搜索许多示例和解释。规范是 JSR 310

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

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

在哪里获取 java.time 类?

原文由 Basil Bourque 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题