155

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

一点背景:

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

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

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

所以我的问题是:

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

4

12 回答 12

134

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

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

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

(根据需要等)

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

于 2010-01-04T19:43:46.610 回答
99
于 2014-10-26T07:54:01.903 回答
44

正如乔恩斯基特所说

“使用 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,它将提供实现以及标准接口。完整的 jar 只有 96kB,所以它不应该破坏银行......

于 2010-01-04T19:48:47.723 回答
15

虽然使用某些 DateFactory 模式看起来不错,但它并不涵盖您无法控制的库 - 想象一下 Validation annotation @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的原始未模拟值,所以我们使用纳米计时器 - 这与挂钟无关,但相对时间在这里就足够了:

// 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 选项可以在不同的时间运行,那就太好了,太糟糕了,如果没有这样的 hack 是不可能的。

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

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

编辑 2014 年 7 月:JMockit 最近发生了很大变化,您必须使用 JMockit 1.0 才能正确使用它(IIRC)。绝对不能升级到界面完全不同的最新版本。我正在考虑只内联必要的东西,但由于我们在新项目中不需要这个东西,所以我根本没有开发这个东西。

于 2013-01-02T10:05:24.843 回答
8

Powermock效果很好。只是用它来模拟System.currentTimeMillis().

于 2010-04-03T18:27:12.613 回答
6

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

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

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

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

于 2010-01-04T19:49:01.293 回答
6

在使用 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();
    }
  }
  // (...) 
}

测试课需要做什么

第 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实例并在比较指令中使用,然后已正确重置为当前日期initDefaultClock()

于 2017-08-21T14:46:34.183 回答
3

在我看来,只有非侵入性的解决方案才能奏效。特别是如果您有外部库和大量遗留代码库,则没有可靠的方法来模拟时间。

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

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

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

@jarnbjo:你能展示一些javaagent代码吗?

于 2012-06-23T22:46:32.433 回答
3

这是一个使用 PowerMockito 的示例。还有一个new Date()的例子。有关模拟系统类
的 更多详细信息。

@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
于 2021-04-02T14:28:51.453 回答
2

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

于 2010-01-04T19:43:30.757 回答
2

如果你运行的是 Linux,你可以使用 libfaketime 的 master 分支,或者在测试时提交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 托管服务的时间,只需将以下内容添加到您的单元文件覆盖中,例如对于弹性搜索,这将是/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

于 2017-11-03T09:41:45.767 回答
-1

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

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

于 2015-08-12T10:12:31.147 回答