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

2020/11/21 14:42 · java ·  · 0评论

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

一点背景:

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

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

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

所以我的问题是:

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

提前致谢!

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

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

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

(根据需要等)

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

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…方法产生的时钟以不同的节奏增加当前时刻。

在Java 8中,默认值Clock报告的时间更新为毫秒,而在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显示的时钟时间早于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类?

正如乔恩·斯凯特(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,它将提供实现以及标准接口。完整的jar仅为96kB,所以它不会破坏资金...

虽然使用某些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);

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

Powermock很棒。只是用它来嘲笑System.currentTimeMillis()

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

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

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

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

在具有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()

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

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

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

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

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

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

如果您正在运行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

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

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

本文地址:http://java.askforanswer.com/fugaijava-system-currenttimemillisyiceshiduishijianmingandedaima.html
文章标签: ,   ,   ,  
版权声明:本文为原创文章,版权归 admin 所有,欢迎分享本文,转载请保留出处!

文件下载

老薛主机终身7折优惠码boke112

上一篇:
下一篇:

评论已关闭!