重写Java System.currentTimeMillis以测试时间敏感代码
除了手动更改主机上的系统时钟外,是否有一种方法可以通过代码或JVM参数覆盖当前时间(通过重写Java System.currentTimeMillis以测试时间敏感代码,java,testing,jvm,systemtime,Java,Testing,Jvm,Systemtime,除了手动更改主机上的系统时钟外,是否有一种方法可以通过代码或JVM参数覆盖当前时间(通过System.currentTimeMillis显示) 一点背景: 我们有一个系统,可以运行许多会计工作,这些工作的逻辑大部分围绕当前日期(即每月1日、每年1日等) 不幸的是,许多遗留代码调用函数,如newdate()或Calendar.getInstance(),这两个函数最终都会调用System.currentTimeMillis 出于测试目的,现在我们不得不手动更新系统时钟,以操纵代码认为测试正在运行的
System.currentTimeMillis
显示)
一点背景:
我们有一个系统,可以运行许多会计工作,这些工作的逻辑大部分围绕当前日期(即每月1日、每年1日等)
不幸的是,许多遗留代码调用函数,如newdate()
或Calendar.getInstance()
,这两个函数最终都会调用System.currentTimeMillis
出于测试目的,现在我们不得不手动更新系统时钟,以操纵代码认为测试正在运行的时间和日期
所以我的问题是:
是否有方法覆盖系统返回的内容。currentTimeMillis?例如,告诉JVM在从该方法返回之前自动添加或减去某个偏移量
提前谢谢 在VM中确实没有直接执行此操作的方法,但是您可以在测试机器上通过编程方式设置系统时间。大多数(所有?)操作系统都有命令行命令来执行此操作。我强烈建议您不要打乱系统时钟,而是咬紧牙关,重构旧代码以使用可替换的时钟。理想情况下,这应该通过依赖注入来实现,但即使使用可替换的单例,也可以获得可测试性 这几乎可以通过搜索和替换singleton版本实现自动化:
- 将
替换为Calendar.getInstance()
Clock.getInstance().getCalendarInstance()
- 将
替换为newDate()
Clock.getInstance().newDate()
- 将
System.currentTimeMillis()替换为
Clock.getInstance().currentTimeMillis()
new Date()
替换为new DateTime().toDate()
)
如果您想要导入一个具有接口的库(请参见下面Jon的评论),您可以使用,它将提供实现和标准接口。完整的jar只有96kB,所以它不应该破坏数据库…使用面向方面编程(AOP,例如AspectJ)来编织系统类,以返回一个预定义的值,您可以在测试用例中设置该值 或者编织应用程序类,将调用重定向到
System.currentTimeMillis()
或new Date()
到您自己的另一个实用程序类
但是,编织系统类(java.lang.
)要稍微复杂一些,您可能需要为rt.jar执行脱机编织,并为测试使用单独的JDK/rt.jar
它被称为二进制编织,还有一些特殊的方法来执行系统类的编织并避免一些问题(例如,引导虚拟机可能不起作用)效果很好。只是用它来模拟System.currentTimeMillis() 在我看来,只有非侵入性的解决方案才能奏效。特别是如果您有外部lib和大量遗留代码库,就没有可靠的方法来模拟时间 JMockit。。。仅适用于有限数量的 PowerMock&Co…需要模拟到System.currentTimeMillis()的系统。这也是一种侵入性的选择 从这里,我只看到提到的javaagent或aop方法对整个系统是透明的。有没有人这样做过,并能指出这样一个解决方案
@jarnbjo:您能展示一些javaagent代码吗?虽然使用一些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在多次呼叫后时间恢复正常-以下是问题报告:
要克服这一点,我们必须启用一个特定的热点优化—使用此参数运行JVM-XX:-Inline
虽然这对于生产来说可能并不完美,但对于测试来说它是很好的,对于应用程序来说它是绝对透明的,特别是当DataFactory没有商业意义并且只是因为测试而引入时。如果有内置的JVM选项可以在不同的时间运行,那就太好了,但如果没有这样的攻击,这是不可能的
完整的故事在我的博客帖子中:
邮局提供了完整的方便类SystemTimeShift。类可以在测试中使用,也可以很容易地将它用作真正的主类之前的第一个主类,以便在不同的时间运行应用程序(甚至整个appserver)。当然,这主要是为了测试目的,而不是为了生产环境
编辑2014年7月:JMockit最近变化很大,您必须使用JMockit 1.0才能正确使用它(IIRC)。绝对不能升级到界面完全不同的最新版本。我在考虑只内联必要的东西,但由于我们在新项目中不需要这个东西,所以我根本不开发这个东西。tl;博士
是否有一种方法,无论是在代码中还是在JVM参数中,都可以覆盖通过System.currentTimeMillis显示的当前时间,而不是手动覆盖
// 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;
}
Instant.now(
Clock.fixed(
Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
)
)
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 );
ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );
ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );
System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );
Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );
public static Instant now() {
return Clock.systemUTC().instant();
}
public static ZonedDateTime now() {
return now(Clock.systemDefaultZone());
}
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
}
// (...)
}
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();
}
}
// (...)
}
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(..)
}
}
# 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
[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"
@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