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

时间:2010-01-04 19:39:53

标签: java testing jvm systemtime

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

一点背景:

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

不幸的是,许多遗留代码会调用new Date()Calendar.getInstance()等函数,这两种函数最终都会调用System.currentTimeMillis

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

所以我的问题是:

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

提前致谢!

12 个答案:

答案 0 :(得分:102)

强烈建议您不要乱用系统时钟,而是扼杀子弹并重构旧代码以使用可更换的时钟。 理想情况应该通过依赖注入来完成,但即使您使用了可替换的单例,也可以获得可测试性。

这可以通过搜索和替换单身版本来实现自动化:

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

(根据需要等)

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

答案 1 :(得分:58)

TL;博士

  

有没有办法,无论是在代码中还是在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(具体取决于您的硬件)。您可以要求以不同的粒度报告真实的当前时刻。

假时钟

某些时钟可能存在,产生的结果与主机操作系统硬件时钟的结果不同。

  • fixed - 报告一个不变的(非递增)时刻作为当前时刻。
  • offset - 报告当前时刻,但按照传递的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实施分配的时区。这在某些测试中可能很有用。但我不建议在生产代码中使用此代码,您应该始终明确指定可选的ZoneOffsetZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () ); 参数。

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

continent/region

您可以指定任何特定时区。以Pacific/Auckland格式指定live code in IdeOne.com,例如proper time zone nameAmerica/MontrealEST。切勿使用诸如ISTZonedDateTime 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.nowZonedDateTime.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带来了一种新的实现方式,能够以纳秒的分辨率捕获当前时刻 - 当然,这取决于计算机硬件时钟的能力。


关于 java.time

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

现在位于CalendarSimpleDateFormat项目建议迁移到Joda-Time类。

要了解详情,请参阅maintenance mode。并搜索Stack Overflow以获取许多示例和解释。规范是java.time

您可以直接与数据库交换 java.time 对象。使用符合Oracle Tutorial或更高版本的JSR 310。不需要字符串,不需要{{1}}类。

从哪里获取java.time类?

  • JDBC driverJDBC 4.2以及之后
    • 内置。
    • 带有捆绑实现的标准Java API的一部分。
    • Java 9增加了一些小功能和修复。
  • Java SE 8Java SE 9
    • 大部分java.time功能都被反向移植到Java 6& 7 {in Java SE 6
  • Java SE 7
    • 更新版本的Android捆绑java.time类的实现。
    • 对于早期的Android(< 26),ThreeTen-Backport项目会调整 ThreeTen-Backport (如上所述)。见Android

ThreeTenABP项目使用其他类扩展java.time。该项目是未来可能添加到java.time的试验场。您可以在此处找到一些有用的课程,例如How to use ThreeTenABP…ThreeTen-ExtraIntervalYearWeek

答案 2 :(得分:41)

作为said by 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();

如果要导入具有接口的库(请参阅下面的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)

使用EasyMock在没有Joda Time且没有PowerMock的Java 8 Web应用程序中覆盖当前系统时间以进行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
  }
  // (...)
}

第2步

将新属性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();
    }
  }
  // (...) 
}

在测试类中需要做什么

第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(..)
    }
  }

在调试模式下检查它,您将看到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框架的测试的更多细节。