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


129

System.currentTimeMillis除了在主机上手动更改系统时钟外,是否可以通过代码或JVM参数覆盖通过(通过)显示的当前时间?

一点背景:

我们有一个系统,该系统运行许多会计工作,这些工作围绕其当前日期(即每月的1号,每年的1号等)进行逻辑转换。

不幸的是,许多传统代码都调用诸如new Date()或的函数Calendar.getInstance(),而这两个函数最终都调用到System.currentTimeMillis

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

所以我的问题是:

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

提前致谢!


1
我不知道它是否是相关的了,但没有与AspectJ来实现这一点,看我的答案在另一种方法:stackoverflow.com/questions/18239859/...
Nándor埃尔德菲克特

@NándorElődFekete链接中的解决方案很有趣,但是需要重新编译代码。考虑到他声称正在处理遗留代码,我想知道原始海报是否具有重新编译的能力。
cleberz

1
@cleberz AspectJ的一个不错的特性是可以直接对字节码进行操作,因此不需要原始源代码即可工作。
Nándor埃尔德菲克特

@NándorElődFekete非常感谢您的提示。我没有意识到AspectJ字节码级的检测(尤其是JDK类检测)。我花了一段时间,但我能够确定我必须同时编译rt.jar和非jdk类的加载时,才能适应我的需求(覆盖系统。 currentTimeMillis()和System.nanoTime())。
cleberz

@cleberz对于编织JRE类,我还有另一个答案,您也可以检查一下。
Nándor埃尔德菲克特

Answers:


115

强烈建议您不要硬着头皮,而要硬着头皮,重构那个旧代码以使用可替换的时钟。理想情况下,应该使用依赖项注入来完成,但是即使您使用了可替换的单例,您也将获得可测试性。

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

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

(根据需要等)

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


50
恕我直言,通过抽象或包装所有潜在的API以提高可测试性来弄乱代码不是一个好主意。即使您只需使用简单的搜索和替换即可进行一次重构,代码也变得更加难以阅读,理解和维护。多个模拟框架应该能够修改System.currentTimeMillis的行为,如果不能,则使用AOP或自制工具化是更好的选择。
jarnbjo 2010年

21
@jarnbjo:好的,当然,欢迎您提出意见-但这是IMO一项非常完善的技术,我当然已经用它取得了很大的效果。它不仅提高了可测试性,而且还使您对系统时间的依赖性变得明确,这在获得代码概述时很有用。
乔恩·斯基特

4
@ virgo47:我认为我们必须同意不同意。我看不到“明确声明您的依赖项” 污染了代码-我认为这是使代码更清晰的自然方法。我也没有看到通过静态调用将那些依赖项隐藏为“完全可以接受”。
乔恩·斯基特

26
更新 Java 8中内置的新java.time包包括一个java.time.Clock类“以允许在需要时插入备用时钟”。
罗勒·布尔克

7
这个答案暗示DI很好。就个人而言,我还没有在现实世界中看到可以充分利用它的应用程序。取而代之的是,我们看到大量的无意义的独立接口,无状态的“对象”,仅数据的“对象”和低内聚性类。IMO,简单性和面向对象的设计(带有真实的全状态对象)是更好的选择。关于static方法,确保它们不是面向对象的,但是注入其实例不对任何实例状态进行操作的无状态依赖并不是真的更好。无论如何,这只是掩饰有效的“静态”行为的一种奇特方法。
罗杰里奥

78

tl; dr

除了通过手动更改主机上的系统时钟之外,是否可以通过代码或使用JVM参数覆盖通过System.currentTimeMillis显示的当前时间?

是。

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

Clock 在java.time中

我们为可插拔时钟替换问题提供了一种新的解决方案,以方便使用人造日期时间值进行测试。该java.time包的Java 8包含一个抽象类java.time.Clock,有明确的目的:

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

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

节奏改变

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

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

错误的时钟

可能存在一些时钟,产生的结果与主机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 );

使用该特殊的固定时钟始终返回同一时刻。我们在基里蒂马蒂(Kiritimati)取得圣诞节的第一刻,UTC显示的时钟时间早于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 [太平洋/基里提马蒂]

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

真实时间,不同时区

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

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

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

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

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

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。亲自查看Java的预发行版9源代码Instant.now

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

默认ClockOffsetDateTime.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.DateCalendar,和SimpleDateFormat

要了解更多信息,请参见Oracle教程。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

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

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

在哪里获取java.time类?


这是一个正确的答案
石羊

42

正如乔恩·斯凯特(Jon Skeet)所说

对于任何涉及“如何使用java.util.Date/Calendar实现X的问题”的问题,“使用Joda Time”几乎总是最佳答案。

所以这里去(假设你刚刚更换所有的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的Clock,它将提供实现以及标准接口。完整的罐子只有96kB,所以它不会破坏资金...


13
不,我不建议您这样做-它不会使您趋向于真正可更换的时钟;它使您始终使用静态。使用Joda Time当然是个好主意,但我仍然想使用Clock接口或类似接口。
乔恩·斯基特

@Jon:是的-对于测试,可以,但是最好有一个接口。
斯蒂芬

5
@Jon:如果要测试已经使用JodaTime的时间相关代码,这是一种平滑,恕我直言的完美简便的方法。如果已经使用JodaTime为您解决了所有问题,则尝试通过引入另一个抽象层/框架来重塑轮子是不可能的。
Stefan Haberl

2
@JonSkeet:这不是Mike最初要的。他想要一种工具来测试时间相关代码,而不是生产代码中成熟的可替换时钟。我更喜欢使我的体系结构尽可能复杂,但又尽可能简单。根本没有必要在此处引入附加的抽象层(即接口)
Stefan Haberl 2012年

2
@StefanHaberl:有很多事情并不是严格意义上的“必要”,但实际上我的建议是使已经存在的依赖关系变得明确,并且更容易进行隔离测试。用静态伪装系统时间具有所有正常的静态问题- 无缘无故是全局状态。Mike要求一种使用当前日期和时间编写可测试代码的方法。我仍然相信我的建议比有效地伪造一个静态方法更加清晰(并且使依赖项更清晰)。
乔恩·斯基特

15

虽然使用某些DateFactory模式看起来不错,但它并不涵盖您无法控制的库-想象一下Validation批注@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);

因为不可能达到原始的毫厘秒值,所以我们改用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优化-使用此参数运行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)。绝对不能升级到界面完全不同的最新版本。我当时正在考虑仅内联必要的东西,但是由于我们在新项目中不需要此东西,所以我根本没有开发此东西。


1
更改每个类的时间的事实是双向的……例如,如果要模拟nanoTime而不是currentTimeMillis,则必须非常小心,不要破坏java.util.concurrent。根据经验,如果在测试中很难使用第三方库,则不应该模拟库的输入:您应该模拟库本身。
Dan Berindei

1
我们在没有其他任何模拟的测试中使用此技术。这是集成测试,我们测试整个堆栈是否在一年中的某些活动首次发生时是否应做的工作,等等。关于nanoTime的要点,幸运的是,由于它没有挂钟,我们不需要模拟那个堆栈的意思。我很乐意看到Java具有时移功能,因为我已经不止一次需要它们,并且由于这很不方便而弄乱了系统时间。
virgo47

7

Powermock的效果很好。只是用它来嘲笑System.currentTimeMillis()


我尝试使用Powermock,如果我正确理解它,就不会真正嘲笑被调用方。相反,它将查找所有调用者,然后应用该模拟。这可能会非常耗时,因为如果您想真正确保应用程序在轮换的时间内工作,则必须让它检查类路径中的每个类。如果我对Powermock的模拟方式不正确,请纠正我。
virgo47

1
@ virgo47不,您理解错误。PowerMock绝不会“检查您的类路径中的每个类”;它只会检查您明确要求检查的类(通过@PrepareForTest测试类上的注释)。
罗格里奥2014年

1
@Rogério-我就是这么理解的-我说“你必须让它...”。也就是说,如果您希望调用System.currentTimeMillis类路径中的任何位置(任何lib),则必须检查每个类。那就是我的意思,别无其他。关键是您在调用方方面嘲笑了该行为(“您专门告诉”)。对于简单测试,这是可以的,但对于不能确定从何处调用该方法的测试,则不能(例如,涉及库的更复杂的组件测试)。这并不意味着Powermock完全是错误的,只是意味着您不能将其用于此类测试。
virgo47,2014年

1
@ virgo47是的,我明白您的意思。我以为您的意思是PowerMock必须自动检查类路径中的每个类,这显然是荒谬的。但是实际上,对于单元测试,我们通常知道哪个类读取时间,因此指定时间不是问题。对于集成测试,确实,PowerMock中要求指定所有使用的类的要求可能是一个问题。
罗杰里奥

6

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

或编织应用程序类的呼叫重定向到System.currentTimeMillis()new Date()以其他实用程序类你自己的。

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

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


当然,这可行,但是使用模拟工具比使用AOP工具容易得多。对于所讨论的目的,后一种工具太通用了。
罗杰里奥

3

确实没有直接在VM中执行此操作的方法,但是您可以通过编程方式在测试计算机上设置系统时间。大多数(所有?)操作系统都具有命令行命令来执行此操作。


例如,在Windows上,dateand time命令。在Linux上,该date命令。
杰里米·雷蒙德

为什么不可以通过AOP或工具化来做到这一点呢?
jarnbjo 2010年

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注入到需要当前日期时间的方法中。例如,在我的情况下,我必须检查存储在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();
    }
  }
  // (...) 
}

测试班需要做什么

第三步

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


2

我认为,只有无创解决方案才能奏效。特别是如果您具有外部库和大量的旧代码库,则没有可靠的方法可以节省时间。

JMockit ...仅适用于有限次数

PowerMock&Co ...需要将客户端模拟为System.currentTimeMillis()。再次是一种侵入性选择。

从这里我只看到提到的javaagentaop方法对整个系统都是透明的。是否有人这样做并可能指出这样的解决方案?

@jarnbjo:您能显示一些javaagent代码吗?


3
jmockit解决方案可以使用-XX来运行无限次:-Inline hack(在Sun的HotSpot上):virgo47.wordpress.com/2012/06/22/changing-system-time-in-java提供了完整的便捷类SystemTimeShifter在文中。可以在测试中使用该类,也可以将其用作真正的主要类之前的第一个主要类,以便在不同的时间运行您的应用程序(甚至整个应用服务器)。当然,这主要是出于测试目的,而不是针对生产环境。
virgo47

2

如果您正在运行Linux,则可以使用libfaketime的master分支,或者在测试commit 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的master分支。

如果您想更改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


-1

如果要模拟具有System.currentTimeMillis()参数的方法,则可以传递anyLong()Matchers类作为参数。

PS:我能够使用上述技巧成功运行我的测试用例,并且可以分享有关我正在使用PowerMock和Mockito框架进行的测试的更多详细信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.