programing tip

시간에 민감한 코드를 테스트하기 위해 Java System.currentTimeMillis를 재정의하십시오.

itbloger 2020. 7. 18. 10:37
반응형

시간에 민감한 코드를 테스트하기 위해 Java System.currentTimeMillis를 재정의하십시오.


System.currentTimeMillis호스트 컴퓨터에서 시스템 시계를 수동으로 변경하는 것 외에 를 통해 제시된대로 코드에서 또는 JVM 인수로 현재 시간을 재정의하는 방법이 있습니까?

작은 배경 :

현재 날짜 (예 : 월 1 일, 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

호스트 컴퓨터에서 시스템 시계를 수동으로 변경하는 것 외에 System.currentTimeMillis를 통해 표시되는 현재 시간을 코드 또는 JVM 인수로 사용하여 현재 시간을 재정의하는 방법이 있습니까?

예.

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

Clock java.time에서

가짜 날짜-시간 값으로 테스트를 용이하게하기 위해 플러그 가능 클록 교체 문제에 대한 새로운 솔루션이 있습니다. java.time 패키지 에서 자바 (8)는 추상 클래스 포함 java.time.Clock명시 적 목적을 :

필요에 따라 대체 클럭을 연결할 수 있도록

의 구현을 꽂을 수 Clock있지만 필요에 맞게 이미 구현 된 것을 찾을 수도 있습니다. 편의상 java.time에는 특수 구현을 생성하는 정적 메소드가 포함되어 있습니다. 이러한 대체 구현은 테스트 중에 유용 할 수 있습니다.

변경된 케이던스

다양한 tick…방법은 다른 케이던스로 현재 순간을 증가시키는 클럭을 생성합니다.

기본값 은 하드웨어에 따라 Java 8 및 Java 9에서 밀리 초 단위나노초 단위Clock업데이트 된 시간을보고합니다 . 실제 세분을 다른 세분성으로보고하도록 요청할 수 있습니다.

거짓 시계

일부 클럭은 거짓말로 인해 호스트 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 시보 다 일찍 14 시간 일찍 벽시계 시간표시합니다 .

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구현에서 할당 한 시간대를 제어 할 수 있습니다 . 이것은 일부 테스트에 유용 할 수 있습니다. 그러나 프로덕션 코드에서는 항상 선택 사항 ZoneId또는 ZoneOffset인수를 명시 적으로 지정 해야하는 것이 좋습니다 .

UTC를 기본 영역으로 지정할 수 있습니다.

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

특정 시간대를 지정할 수 있습니다. 지정 적절한 시간대 이름 의 형식 continent/region예컨대, America/Montreal, Africa/Casablanca, 또는 Pacific/Auckland. 표준 시간대 아니EST 거나 표준화되지 않았으며 고유하지 않은 3-4 문자 약어를 사용하지 마십시오 .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 [미국 / 로스 앤젤레스]

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.now있다 Clock.systemDefaultZone(). 소스 코드를 참조하십시오 .

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

기본 구현의 동작은 Java 8과 Java 9 사이에서 변경되었습니다. Java 8 에서 클래스의 나노초 분해능 저장 기능에도 불구하고 현재 순간은 밀리 초 단위 의 해상도로 캡처됩니다 . Java 9는 물론 컴퓨터 하드웨어 시계의 기능에 따라 나노초의 해상도로 현재 순간을 캡처 할 수있는 새로운 구현을 제공합니다.


java.time에 대하여

java.time의 프레임 워크는 나중에 자바 8에 내장되어 있습니다. 이 클래스는 까다로운 기존에 대신 기존 과 같은 날짜 - 시간의 수업을 java.util.Date, Calendar, SimpleDateFormat.

Joda 타임 프로젝트는 지금에 유지 관리 모드 의로 마이그레이션을 조언 java.time의 클래스.

자세한 내용은 Oracle Tutorial을 참조하십시오 . 많은 예제와 설명을 보려면 스택 오버플로를 검색하십시오. 사양은 JSR 310 입니다.

java.time 객체를 데이터베이스와 직접 교환 할 수 있습니다 . JDBC 4.2 이상을 준수 하는 JDBC 드라이버를 사용하십시오 . 문자열이 필요없고 수업이 필요 없습니다 .java.sql.*

java.time 클래스는 어디서 구할 수 있습니까?

ThreeTen - 추가 프로젝트 추가 클래스와 java.time를 확장합니다. 이 프로젝트는 향후 java.time에 추가 될 수있는 입증 된 근거입니다. 당신은 여기에 몇 가지 유용한 클래스와 같은 찾을 수 있습니다 Interval, YearWeek, YearQuarter, 그리고 .


으로는 존 소총 말했다 :

"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을 사용할 수 있습니다. 전체 병은 96kB에 불과하므로 은행을 파기해서는 안됩니다 ...


일부 DateFactory 패턴을 사용하는 것이 좋지만 제어 할 수없는 라이브러리는 다루지 않습니다 .System.currentTimeMillis에 의존하는 구현으로 유효성 검사 주석 @Past를 상상하십시오 (그렇습니다).

그래서 우리는 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 옵션을 다른 시간에 실행하는 것이 좋을 것입니다. 너무 나빠서 이와 같은 해킹이 없으면 불가능합니다.

전체 기사는 내 블로그 게시물 ( http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/)에 있습니다.

완벽한 편리한 클래스 SystemTimeShifter가 게시물에 제공됩니다. 테스트에서 클래스를 사용하거나 다른 시간에 응용 프로그램 (또는 전체 응용 프로그램 서버)을 실행하기 위해 실제 메인 클래스보다 첫 번째 메인 클래스로 매우 쉽게 사용할 수 있습니다. 물론 이것은 프로덕션 환경이 아니라 주로 테스트 목적으로 사용됩니다.

2014 년 7 월 편집 : JMockit은 최근에 많이 바뀌었고 이것을 올바르게 사용하려면 JMockit 1.0을 사용해야합니다 (IIRC). 인터페이스가 완전히 다른 최신 버전으로 확실히 업그레이드 할 수 없습니다. 나는 필요한 것들을 인라인하는 것에 대해 생각하고 있었지만, 새로운 프로젝트에서 이것을 필요로하지 않기 때문에 나는 이것을 전혀 개발하지 않고 있습니다.


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


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)


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.


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


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?


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


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.

참고URL : https://stackoverflow.com/questions/2001671/override-java-system-currenttimemillis-for-testing-time-sensitive-code

반응형