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

发布于 2024-08-16 09:12:30 字数 443 浏览 3 评论 0 原文

除了手动更改主机上的系统时钟之外,是否有一种方法(通过代码或使用 JVM 参数)来覆盖当前时间(如 System.currentTimeMillis 所示)?

一点背景知识:

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

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

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

所以我的问题是:

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

Is there a way, either in code or with JVM arguments, to override the current time, as presented via System.currentTimeMillis, other than manually changing the system clock on the host machine?

A little background:

We have a system that runs a number of accounting jobs that revolve much of their logic around the current date (ie 1st of the month, 1st of the year, etc)

Unfortunately, a lot of the legacy code calls functions such as new Date() or Calendar.getInstance(), both of which eventually call down to System.currentTimeMillis.

For testing purposes, right now, we are stuck with manually updating the system clock to manipulate what time and date the code thinks that the test is being run.

So my question is:

Is there a way to override what is returned by System.currentTimeMillis? For example, to tell the JVM to automatically add or subtract some offset before returning from that method?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

指尖上的星空 2024-08-23 09:12:30

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

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

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

(根据需要等)

一旦迈出了第一步,您就可以用 DI 一次一点地替换单例。

I strongly recommend that instead of messing with the system clock, you bite the bullet and refactor that legacy code to use a replaceable clock. Ideally that should be done with dependency injection, but even if you used a replaceable singleton you would gain testability.

This could almost be automated with search and replace for the singleton version:

  • Replace Calendar.getInstance() with Clock.getInstance().getCalendarInstance().
  • Replace new Date() with Clock.getInstance().newDate()
  • Replace System.currentTimeMillis() with Clock.getInstance().currentTimeMillis()

(etc as required)

Once you've taken that first step, you can replace the singleton with DI a bit at a time.

丘比特射中我 2024-08-23 09:12:30

太长了;博士

除了手动更改主机上的系统时钟之外,是否有一种方法(通过代码或使用 JVM 参数)来覆盖当前时间(通过 System.currentTimeMillis 提供)?

是的。

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

java.time 中的Clock

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

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

您可以插入您自己的 Clock 实现,尽管您可能会找到一个已经实现的时钟来满足您的需求。为了您的方便,java.time 包含静态方法来产生特殊的实现。这些替代实现在测试期间可能很有价值。

改变节奏

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

默认时钟报告时间更新频率为毫秒在 Java 8 和 Java 9 中与 纳秒 一样精细(取决于您的硬件)。您可以要求以不同的粒度报告当前的真实时刻。

虚假时钟

有些时钟可能会撒谎,产生与主机操作系统硬件时钟不同的结果。

  • fixed - 将单个不变(非增量)时刻报告为当前时刻。
  • offset - 报告当前时刻,但按传递的 持续时间 参数。

比如锁定今年最早圣诞节的第一时刻。换句话说,当圣诞老人和他的驯鹿到达第一站时。现在最早的时区似乎是 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 显示挂钟时间 提前 14 个小时,即 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 () );

您可以指定任何特定时区。以大陆/地区格式指定正确的时区名称 ,例如 America/Montreal非洲/卡萨布兰卡,或太平洋/奥克兰 。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们不是真正的时区,不是标准化的,甚至不是唯一的( !)。

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[美国/洛杉矶]

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()。这是当您不指定时钟时使用的实现。亲自查看 Instant.now 的预发布 Java 9 源代码。

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

OffsetDateTime.nowZonedDateTime.now 的默认ClockClock.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.Date日历, & SimpleDateFormat

要了解更多信息,请参阅 Oracle 教程。并在 Stack Overflow 上搜索许多示例和解释。规范为 JSR 310

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

您可以直接与数据库交换java.time对象。使用符合 JDBC 驱动程序 /jeps/170" rel="nofollow noreferrer">JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* 类。 Hibernate 5 和 Hibernate 5 JPA 2.2 支持 java.time。

从哪里获取 java.time 类?

tl;dr

Is there a way, either in code or with JVM arguments, to override the current time, as presented via System.currentTimeMillis, other than manually changing the system clock on the host machine?

Yes.

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

Clock In java.time

We have a new solution to the problem of a pluggable clock replacement to facilitate testing with faux date-time values. The java.time package in Java 8 includes an abstract class java.time.Clock, with an explicit purpose:

to allow alternate clocks to be plugged in as and when required

You could plug in your own implementation of Clock, though you likely can find one already made to meet your needs. For your convenience, java.time includes static methods to yield special implementations. These alternate implementations can be valuable during testing.

Altered cadence

The various tick… methods produce clocks that increment the current moment with a different cadence.

The default Clock reports a time updated as frequently as milliseconds in Java 8 and in Java 9 as fine as nanoseconds (depending on your hardware). You can ask for the true current moment to be reported with a different granularity.

False clocks

Some clocks can lie, producing a result different than that of the host OS’ hardware clock.

  • fixed - Reports a single unchanging (non-incrementing) moment as the current moment.
  • offset - Reports the current moment but shifted by the passed Duration argument.

For example, lock in the first moment of the earliest Christmas this year. in other words, when Santa and his reindeer make their first stop. The earliest time zone nowadays seems to be Pacific/Kiritimati at +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 );

Use that special fixed clock to always return the same moment. We get the first moment of Christmas day in Kiritimati, with UTC showing a wall-clock time of fourteen hours earlier, 10 AM on the prior date of the 24th of December.

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[Pacific/Kiritimati]

See live code in IdeOne.com.

True time, different time zone

You can control which time zone is assigned by the Clock implementation. This might be useful in some testing. But I do not recommend this in production code, where you should always specify explicitly the optional ZoneId or ZoneOffset arguments.

You can specify that UTC be the default zone.

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

You can specify any particular time zone. Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

You can specify the JVM’s current default time zone should be the default for a particular Clock object.

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

Run this code to compare. Note that they all report the same moment, the same point on the timeline. They differ only in wall-clock time; in other words, three ways to say the same thing, three ways to display the same moment.

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles was the JVM current default zone on the computer that ran this code.

zdtClockSystemUTC.toString(): 2016-12-31T20:52:39.688Z

zdtClockSystem.toString(): 2016-12-31T15:52:39.750-05:00[America/Montreal]

zdtClockSystemDefaultZone.toString(): 2016-12-31T12:52:39.762-08:00[America/Los_Angeles]

The Instant class is always in UTC by definition. So these three zone-related Clock usages have exactly the same effect.

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

Default clock

The implementation used by default for Instant.now is the one returned by Clock.systemUTC(). This is the implementation used when you do not specify a Clock. See for yourself in pre-release Java 9 source code for Instant.now.

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

The default Clock for OffsetDateTime.now and ZonedDateTime.now is Clock.systemDefaultZone(). See source code.

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

The behavior of the default implementations changed between Java 8 and Java 9. In Java 8, the current moment is captured with a resolution only in milliseconds despite the classes’ ability to store a resolution of nanoseconds. Java 9 brings a new implementation able to capture the current moment with a resolution of nanoseconds – depending, of course, on the capability of your computer hardware clock.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes. Hibernate 5 & JPA 2.2 support java.time.

Where to obtain the java.time classes?

煮茶煮酒煮时光 2024-08-23 09:12:30

正如 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();

如果您想导入具有接口的库(请参阅下面乔恩的评论),您可以使用 Prevayler's Clock,将提供实现以及标准接口。完整的 jar 只有 96kB,所以它不应该花很多钱......

As said by Jon Skeet:

"use Joda Time" is almost always the best answer to any question involving "how do I achieve X with java.util.Date/Calendar?"

So here goes (presuming you've just replaced all your new Date() with 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();

If you want import a library that has an interface (see Jon's comment below), you could just use Prevayler's Clock, which will provide implementations as well as the standard interface. The full jar is only 96kB, so it shouldn't break the bank...

浅听莫相离 2024-08-23 09:12:30

虽然使用某些 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 优化 - 使用此参数运行 JVM -XX:-内联

虽然这对于生产来说可能并不完美,但它对于测试来说已经足够了,并且对于应用程序来说绝对透明,特别是当 DataFactory 没有商业意义并且只是因为测试而引入时。如果有内置的 JVM 选项在不同的时间运行就好了,可惜没有这样的 hacks 是不可能的。

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

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

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

While using some DateFactory pattern seems nice, it does not cover libraries you can't control - imagine Validation annotation @Past with implementation relying on System.currentTimeMillis (there is such).

That's why we use jmockit to mock the system time directly:

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

Because it's not possible to get to the original unmocked value of millis, we use nano timer instead - this is not related to wall clock, but relative time suffices here:

// 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;
}

There is documented problem, that with HotSpot the time gets back to normal after a number of calls - here is the issue report: http://code.google.com/p/jmockit/issues/detail?id=43

To overcome this we have to turn on one specific HotSpot optimization - run JVM with this argument -XX:-Inline.

While this may not be perfect for production, it is just fine for tests and it is absolutely transparent for application, especially when DataFactory doesn't make business sense and is introduced only because of tests. It would be nice to have built-in JVM option to run in different time, too bad it is not possible without hacks like this.

Complete story is in my blog post here:
http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

Complete handy class SystemTimeShifter is provided in the post. Class can be used in your tests, or it can be used as the first main class before your real main class very easily in order to run your application (or even whole appserver) in a different time. Of course, this is intented for testing purposes mainly, not for production environment.

EDIT July 2014: JMockit changed a lot lately and you are bound to use JMockit 1.0 to use this correctly (IIRC). Definitely can't upgrade to newest version where interface is completly different. I was thinking about inlining just the necessary stuff, but as we don't need this thing in our new projects I'm not developing this thing at all.

花想c 2024-08-23 09:12:30

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

Powermock works great. Just used it to mock System.currentTimeMillis().

橘和柠 2024-08-23 09:12:30

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

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

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

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

Use Aspect-Oriented Programming (AOP, for example AspectJ) to weave the System class to return a predefined value which you could set within your test cases.

Or weave the application classes to redirect the call to System.currentTimeMillis() or to new Date() to another utility class of your own.

Weaving system classes (java.lang.*) is however a little bit more trickier and you might need to perform offline weaving for rt.jar and use a separate JDK/rt.jar for your tests.

It's called Binary weaving and there are also special tools to perform weaving of System classes and circumvent some problems with that (e.g. bootstrapping the VM may not work)

天荒地未老 2024-08-23 09:12:30

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

以下是您需要做的事情:

在被测试的类中需要做的事情

步骤 1

向被测试的类 MyService 添加一个新的 java.time.Clock 属性,并确保新属性将使用实例化块或构造函数以默认值正确初始化:

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() 正确重置为当前日期。

A working way to override current system time for JUnit testing purposes in a Java 8 web application with EasyMock, without Joda Time, and without PowerMock.

Here's what you need to do:

What needs to be done in the tested class

Step 1

Add a new java.time.Clock attribute to the tested class MyService and make sure the new attribute will be initialized properly at default values with an instantiation block or a constructor:

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

Step 2

Inject the new attribute clock into the method which calls for a current date-time. For instance, in my case I had to perform a check of whether a date stored in dataase happened before LocalDateTime.now(), which I remplaced with LocalDateTime.now(clock), like so:

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

What needs to be done in the test class

Step 3

In the test class, create a mock clock object and inject it into the tested class's instance just before you call the tested method doExecute(), then reset it back right afterwards, like so:

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

Check it in debug mode and you will see the date of 2017 Feb 3 has been correctly injected into myService instance and used in the comparison instruction, and then has been properly reset to current date with initDefaultClock().

圈圈圆圆圈圈 2024-08-23 09:12:30

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

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

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

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

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

In my opinion only a none-invasive solution can work. Especially if you have external libs and a big legacy code base there is no reliable way to mock out time.

JMockit ... works only for restricted number of times

PowerMock & Co ...needs to mock the clients to System.currentTimeMillis(). Again an invasive option.

From this I only see the mentioned javaagent or aop approach being transparent to the whole system. Has anybody done that and could point to such a solution?

@jarnbjo: could you show some of the javaagent code please?

枕花眠 2024-08-23 09:12:30

这是使用 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

Here is an example using PowerMockito. Also there is an example with new Date().
More details about 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();
    }

}

Some legacy class you are testing:

class LegacyClass {

    public void methodWithCurrentTimeMillis() {
        long now = System.currentTimeMillis();
        System.out.println("LegacyClass System.currentTimeMillis() is " + now);
    }

}

Console output

Fake currentTimeMillis: 1291371330000
LegacyClass System.currentTimeMillis() is 1291371330000
深海夜未眠 2024-08-23 09:12:30

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

There really isn't a way to do this directly in the VM, but you could all something to programmatically set the system time on the test machine. Most (all?) OS have command line commands to do this.

浅笑依然 2024-08-23 09:12:30

如果您运行的是 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 托管服务的时间,只需将以下内容添加到您的单元文件覆盖中,例如对于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

If you're running Linux, you can use the master branch of libfaketime, or at the time of testing commit 4ce2835.

Simply set the environment variable with the time you'd like to mock your java application with, and run it using 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

The second environment variable is paramount for java applications, which otherwise would freeze. It requires the master branch of libfaketime at the time of writing.

If you'd like to change the time of a systemd managed service, just add the following to your unit file overrides, e.g. for elasticsearch this would be /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"

Don't forget to reload systemd using `systemctl daemon-reload

和影子一齐双人舞 2024-08-23 09:12:30

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

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

If you want to mock the method having System.currentTimeMillis() argument then you can pass anyLong() of Matchers class as an argument.

P.S. I am able to run my test case successfully using the above trick and just to share more details about my test that I am using PowerMock and Mockito frameworks.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文