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

Mik*_*ark 117 java testing jvm systemtime

有没有办法,无论是在代码中还是在JVM参数中,都可以覆盖当前时间,如System.currentTimeMillis手动更改主机上的系统时钟所示?

一点背景:

我们有一个系统可以运行许多会计工作,这些工作围绕当前日期(即本月的第一天,一年中的第一天等)展开大部分逻辑.

不幸的是,很多遗留代码都会调用诸如new Date()or之类的函数,这些函数Calendar.getInstance()最终都会调用System.currentTimeMillis.

出于测试目的,我们现在仍然需要手动更新系统时钟来操作代码认为运行测试的时间和日期.

所以我的问题是:

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

提前致谢!

Jon*_*eet 109

强烈建议您不要乱用系统时钟,而是重复使用可更换的时钟来重构旧代码.理想情况下应该使用依赖注入,但即使您使用了可替换的单例,也可以获得可测试性.

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

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

(等等)

一旦你完成了第一步,你可以一次用DI替换单身.

  • 通过抽象或包装所有潜在的API以提高可测试性来混乱代码是恕我直言,这不是一个好主意.即使您可以通过简单的搜索和替换进行一次重构,代码也会变得更难以阅读,理解和维护.几个模拟框架应该能够修改System.currentTimeMillis的行为,否则,使用AOP或自制的工具化是更好的选择. (39认同)
  • **更新**Java 8中内置的新[java.time包](http://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html)包含一个[` java.time.Clock`](http://docs.oracle.com/javase/8/docs/api/java/time/Clock.html)类"允许在需要时插入备用时钟". (25认同)
  • @jarnbjo:嗯,你当然欢迎你的意见 - 但这是一个非常成熟的技术IMO,我当然用它来产生很大的效果.它不仅提高了可测试性 - 它还使您对系统时间的依赖性明确,这在获取代码概述时非常有用. (19认同)
  • 我不敢相信你没有显示joda-time版本:( (16认同)
  • 这个答案意味着DI都很好.就个人而言,我还没有在现实世界中看到一个充分利用它的应用程序.相反,我们看到大量无意义的单独接口,无状态"对象",仅数据"对象"和低内聚类.IMO,简单和面向对象的设计(使用真实的,有状态的对象)是更好的选择.关于`static`方法,确定它们不是OO,但是注入一个无状态依赖关系,其实例不在任何实例状态上运行并不是更好; 无论如何,这只是掩饰什么是有效"静态"行为的奇特方式. (6认同)
  • @ virgo47:我想我们不得不同意不同意.我没有看到"明确说明你的依赖关系"*污染*代码 - 我认为这是使代码更清晰的自然方式.我也没有看到通过静态调用将这些依赖项隐藏为"完全可接受". (4认同)
  • @Rogério:我们之前已经有了这个论点,我认为我们中的任何一个都不会改变彼此的想法.我认为在每个答案的评论主题中都有相同的论点没有任何好处,这个论点谈到这一点...... (4认同)
  • @ virgo47:对我来说听起来像是一个黑客 - 我宁愿把依赖性弄清楚. (3认同)
  • 对于您可以控制的代码,乔恩的建议是最好的。不幸的是,您可能必须使用您无法控制的库:( (2认同)
  • @PrashantKalkar:到那时,如果没有更多侵入性工具,就很难进行测试。 (2认同)
  • @ azis.mrazish:我在讲这个问题的根本原因,而不仅仅是字面上的问题。(我不确定您的评论中“其他”部分的上下文是什么...如果您建议我定期发布不正确的答案,那是很不礼貌的。) (2认同)
  • @JonSkeet 没有什么个人的,但我认为“解决根本原因”是公共论坛中常见的谬误,而“但是”是我看到 stackoverflow 中接受的此类答案的结果。我来这里是为了看看人们如何解决这个有效的特殊情况:在不替换日期时间管理的情况下测试第 3 方和遗留代码,以及我所看到的“用时钟替换日期..”等等,作为可接受的答案。这一点也不好玩。如果我的这样的答案被接受,为了正义,我宁愿自己删除它。AspectJ 编织必须是这里公认的。 (2认同)
  • @ azis.mrazish:考虑到提出这个问题的人是决定接受什么的人,所以我认为他们是确定对他们最有帮助的人的最佳人选。 (2认同)

Bas*_*que 66

TL;博士

有没有办法在代码或JVM参数中覆盖当前时间,如System.currentTimeMillis所示,而不是手动更改主机上的系统时钟?

是.

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

Clock 在java.time中

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

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

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

改变了节奏

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

默认情况下,Java 8和Java 9中Clock的时间报告频率为毫秒,精确到纳秒 (取决于您的硬件).您可以要求以不同的粒度报告真实的当前时刻.

假时钟

某些时钟可能存在,产生的结果与主机OS的硬件时钟不同.

  • 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 );
Run Code Online (Sandbox Code Playgroud)

使用该特殊固定时钟始终返回相同的时刻.我们在Kiritimati获得了圣诞节的第一个时刻,UTC显示的是12月24日前一天早上十四点钟的挂钟时间.

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );
Run Code Online (Sandbox Code Playgroud)

instant.toString():2016-12-24T10:00:00Z

zdt.toString():2016-12-25T00:00 + 14:00 [太平洋/ Kiritimati]

请参阅IdeOne.com中的实时代码.

真实的时间,不同的时区

您可以控制Clock实施分配的时区.这在某些测试中可能很有用.但我不建议在生产代码中使用它,您应该始终明确指定可选ZoneIdZoneOffset参数.

您可以指定UTC是默认区域.

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );
Run Code Online (Sandbox Code Playgroud)

您可以指定任何特定时区.指定适当的时区名称,格式continent/region,如America/Montreal,Africa/CasablancaPacific/Auckland.切勿使用3-4字母缩写,例如ESTIST因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!).

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Run Code Online (Sandbox Code Playgroud)

您可以指定JVM的当前默认时区应该是特定Clock对象的默认时区.

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );
Run Code Online (Sandbox Code Playgroud)

运行此代码进行比较.请注意,它们都报告了相同的时刻,即时间轴上的相同点.它们的区别仅在于挂钟时间 ; 换句话说,三种说法相同的方式,三种方式来显示同一时刻.

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );
Run Code Online (Sandbox Code Playgroud)

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 [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 () );
Run Code Online (Sandbox Code Playgroud)

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().这是您未指定a时使用的实现Clock.请参阅预发布的Java 9源代码Instant.now.

public static Instant now() {
    return Clock.systemUTC().instant();
}
Run Code Online (Sandbox Code Playgroud)

默认ClockOffsetDateTime.nowZonedDateTime.nowClock.systemDefaultZone().请参阅源代码.

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}
Run Code Online (Sandbox Code Playgroud)

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


关于java.time

java.time框架是建立在Java 8和更高版本.这些类取代麻烦的老传统日期时间类,如java.util.Date,Calendar,和SimpleDateFormat.

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

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

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

从哪里获取java.time类?

ThreeTen-额外项目与其他类扩展java.time.该项目是未来可能添加到java.time的试验场.您可以在此比如找到一些有用的类Interval,YearWeek,YearQuarter,和更多.


Ste*_*hen 42

正如Jon Skeet所说:

"使用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();
Run Code Online (Sandbox Code Playgroud)

如果你想导入一个有接口的库(参见下面的Jon的评论),你可以使用Prevayler的Clock,它将提供实现以及标准接口.满罐只有96kB,所以它不应该破坏银行......

  • 不,我不建议这样做 - 它不会让你走向真正可更换的时钟; 它让你始终使用静态.使用Joda Time当然是个好主意,但我仍然想要使用Clock接口或类似设备. (11认同)
  • @Jon:如果您想测试已经使用JodaTime的时间相关代码,这是一个流畅的IMHO完美而简单的方法.试图通过引入另一个抽象层/框架来重新发明轮子不是要走的路,如果一切都已经用JodaTime为你解决了. (5认同)
  • @JonSkeet:这不是迈克原本要求的.他想要一种工具来测试时间相关的代码,而不是生产代码中完整的可更换时钟.我更喜欢保持我的架构尽可能复杂,但尽可能简单.在这里引入额外的抽象层(即接口)根本不是必需的 (2认同)
  • @StefanHaberl:有许多事情并非严格"必要" - 但我真正建议的是使已经存在的依赖*明确*并且更容易孤立地测试.使用静力学伪造系统时间具有静力学的所有正常问题 - 它是全局状态*没有充分理由*.Mike正在寻找一种方法来编写使用当前日期和时间的可测试代码.我仍然认为我的建议比有效地捏造静态更清晰(并使依赖更清晰). (2认同)

vir*_*o47 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);
Run Code Online (Sandbox Code Playgroud)

因为不可能达到millis的原始未锁定值,我们使用纳米计时器 - 这与挂钟无关,但相对时间足够:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

有记录的问题,使用HotSpot,经过多次调用后,时间恢复正常 - 这是问题报告:http://code.google.com/p/jmockit/issues/detail?id = 43

为了解决这个问题,我们必须打开一个特定的HotSpot优化 - 使用此参数运行JVM -XX:-Inline.

虽然这对于生产来说可能并不完美,但它对于测试来说还是很好的,它对应用程序来说绝对透明,特别是当DataFactory没有商业意义并且仅仅是因为测试而引入时.有内置的JVM选项在不同的时间运行会很不错,太糟糕了,没有像这样的黑客攻击是不可能的.

完整的故事在我的博客文章中:http: //virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

帖子中提供了完整的便捷类SystemTimeShifter.类可以在您的测试中使用,或者可以非常轻松地在真正的主类之前用作第一个主类,以便在不同的时间运行您的应用程序(甚至整个应用程序服务器).当然,这主要用于测试目的,而不是用于生产环境.

编辑2014年7月:JMockit最近发生了很大的变化,你必然会使用JMockit 1.0来正确使用它(IIRC).绝对无法升级到界面完全不同的最新版本.我正在考虑内联必要的东西,但是因为我们在新项目中不需要这个东西,所以我根本就没有开发这个东西.


Ren*_*neS 7

Powermock很棒.只是用它来模拟System.currentTimeMillis().


mha*_*ler 6

使用面向方面编程(AOP,例如AspectJ)编织System类以返回可在测试用例中设置的预定义值.

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

java.lang.*然而,编织系统类()有点棘手,您可能需要为rt.jar执行离线编织并使用单独的JDK/rt.jar进行测试.

它被称为二进制编织,还有一些特殊的工具来执行系统类的编织并避免一些问题(例如,引导VM可能不起作用)


Kir*_*kow 6

在 Java 8 Web 应用程序中使用 EasyMock、没有 Joda Time 和没有 PowerMock 覆盖当前系统时间以进行 JUnit 测试的工作方法。

您需要执行以下操作:

测试类需要做什么

第1步

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
  }
  // (...)
}
Run Code Online (Sandbox Code Playgroud)

第2步

将新属性clock注入到调用当前日期时间的方法中。例如,在我的情况下,我必须检查存储在 dataase 中的日期是否发生在 之前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();
    }
  }
  // (...) 
}
Run Code Online (Sandbox Code Playgroud)

测试类需要做什么

第 3 步

在测试类中,创建一个模拟时钟对象,并在调用测试方法之前将其注入到测试类的实例中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(..)
    }
  }
Run Code Online (Sandbox Code Playgroud)

在调试模式下检查它,您将看到 2017 年 2 月 3 日的日期已正确注入myService实例并用于比较指令,然后已正确重置为当前日期initDefaultClock()