Java 8의 java.time API에서 시간 조롱


답변:


73

가장 가까운 것은 Clock물체입니다. 원하는 시간을 사용하여 (또는 시스템 현재 시간에서) Clock 객체를 만들 수 있습니다. 모든 date.time 개체에는 now현재 시간 대신 시계 개체를 사용하는 오버로드 된 메서드가 있습니다. 따라서 종속성 주입을 사용하여 특정 시간으로 Clock을 주입 할 수 있습니다.

public class MyBean {
    private Clock clock;  // dependency inject
    ...
    public void process(LocalDate eventDate) {
      if (eventDate.isBefore(LocalDate.now(clock)) {
        ...
      }
    }
  }

자세한 내용은 Clock JavaDoc 을 참조하십시오.


11
예. 특히, Clock.fixed동안, 테스트에 유용 Clock.system또는 Clock.systemUTC응용 프로그램에서 사용할 수 있습니다.
Matt Johnson-Pint

8
비 틱 시간으로 설정할 수 있지만 나중에 그 시간을 수정할 수있는 가변 시계가 없다는 점이 부끄러운 일입니다 (조다로이 작업을 수행 할 수 있습니다). 이것은 시간에 민감한 코드를 테스트하는 데 유용합니다. 예를 들어 시간 기반 만료가있는 캐시 또는 미래의 이벤트를 예약하는 클래스입니다.
bacar

2
@bacar Clock은 추상 클래스입니다. 자신 만의 테스트 Clock 구현을 만들 수 있습니다.
Bjarne Boström

나는 그것이 우리가 한 일이라고 믿습니다.
bacar

25

새 클래스를 사용하여 Clock.fixed생성 을 숨기고 테스트를 단순화했습니다.

public class TimeMachine {

    private static Clock clock = Clock.systemDefaultZone();
    private static ZoneId zoneId = ZoneId.systemDefault();

    public static LocalDateTime now() {
        return LocalDateTime.now(getClock());
    }

    public static void useFixedClockAt(LocalDateTime date){
        clock = Clock.fixed(date.atZone(zoneId).toInstant(), zoneId);
    }

    public static void useSystemDefaultZoneClock(){
        clock = Clock.systemDefaultZone();
    }

    private static Clock getClock() {
        return clock ;
    }
}
public class MyClass {

    public void doSomethingWithTime() {
        LocalDateTime now = TimeMachine.now();
        ...
    }
}
@Test
public void test() {
    LocalDateTime twoWeeksAgo = LocalDateTime.now().minusWeeks(2);

    MyClass myClass = new MyClass();

    TimeMachine.useFixedClockAt(twoWeeksAgo);
    myClass.doSomethingWithTime();

    TimeMachine.useSystemDefaultZoneClock();
    myClass.doSomethingWithTime();

    ...
}

4
여러 테스트가 병렬로 실행되고 TimeMachine 시계를 변경하는 경우 스레드 안전성은 어떻습니까?
youri

시계를 테스트 된 개체에 전달하고 시간 관련 메서드를 호출 할 때 사용해야합니다. 그리고 getClock()메서드를 제거 하고 필드를 직접 사용할 수 있습니다. 이 방법은 코드 몇 줄만 추가합니다.
deamon

1
Banktime 또는 TimeMachine?
Emanuele

11

나는 필드를 사용했다

private Clock clock;

그리고

LocalDate.now(clock);

내 프로덕션 코드에서. 그런 다음 단위 테스트에서 Mockito를 사용하여 Clock.fixed ()를 사용하여 Clock을 조롱했습니다.

@Mock
private Clock clock;
private Clock fixedClock;

조롱 :

fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
doReturn(fixedClock.instant()).when(clock).instant();
doReturn(fixedClock.getZone()).when(clock).getZone();

역설:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));

9

나는 Clock당신의 생산 코드 를 혼란스럽게 사용하는 것을 발견했습니다 .

JMockit 또는 PowerMock 을 사용 하여 테스트 코드에서 정적 메서드 호출을 모의 할 수 있습니다 . JMockit의 예 :

@Test
public void testSth() {
  LocalDate today = LocalDate.of(2000, 6, 1);

  new Expectations(LocalDate.class) {{
      LocalDate.now(); result = today;
  }};

  Assert.assertEquals(LocalDate.now(), today);
}

편집 : 여기에 비슷한 질문에 대한 Jon Skeet의 답변에 대한 의견을 읽은 후 과거의 자신에 동의하지 않습니다. 무엇보다도이 주장은 정적 메서드를 모의 할 때 테스트를 패럴 라이즈 할 수 없다는 것을 저에게 확신 시켰습니다.

하지만 레거시 코드를 처리해야하는 경우 정적 모의를 사용할 수 있습니다.


1
"레거시 코드에 정적 모의 사용"주석에 +1. 따라서 새 코드의 경우 종속성 주입에 의존하고 Clock (테스트 용 고정 클록, 프로덕션 런타임 용 시스템 클록)을 주입하는 것이 좋습니다.
David Groomes 2018

1

LocalDate대신 인스턴스 가 필요 합니다 LocalDateTime.
그런 이유로 다음 유틸리티 클래스를 만들었습니다.

public final class Clock {
    private static long time;

    private Clock() {
    }

    public static void setCurrentDate(LocalDate date) {
        Clock.time = date.toEpochDay();
    }

    public static LocalDate getCurrentDate() {
        return LocalDate.ofEpochDay(getDateMillis());
    }

    public static void resetDate() {
        Clock.time = 0;
    }

    private static long getDateMillis() {
        return (time == 0 ? LocalDate.now().toEpochDay() : time);
    }
}

그리고 사용법은 다음과 같습니다.

class ClockDemo {
    public static void main(String[] args) {
        System.out.println(Clock.getCurrentDate());

        Clock.setCurrentDate(LocalDate.of(1998, 12, 12));
        System.out.println(Clock.getCurrentDate());

        Clock.resetDate();
        System.out.println(Clock.getCurrentDate());
    }
}

산출:

2019-01-03
1998-12-12
2019-01-03

프로젝트에서 모든 생성 LocalDate.now()을 대체 Clock.getCurrentDate()했습니다.

봄 부팅 응용 프로그램 이기 때문입니다 . test프로필 실행 전에 모든 테스트에 대해 미리 정의 된 날짜를 설정하십시오.

public class TestProfileConfigurer implements ApplicationListener<ApplicationPreparedEvent> {
    private static final LocalDate TEST_DATE_MOCK = LocalDate.of(...);

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
        if (environment.acceptsProfiles(Profiles.of("test"))) {
            Clock.setCurrentDate(TEST_DATE_MOCK);
        }
    }
}

그리고 spring.factories에 추가하십시오 .

org.springframework.context.ApplicationListener = com.init.TestProfileConfigurer


1

EasyMock을 사용하는 Java 8 웹 애플리케이션에서 JUnit 테스트 목적으로 현재 시스템 시간을 특정 날짜로 재정의하는 작업 방법은 다음과 같습니다.

Joda Time은 확실히 멋지지만 (Stephen, Brian, 우리 세상을 더 나은 곳으로 만들었습니다) 저는 그것을 사용할 수 없었습니다.

몇 가지 실험 끝에 결국 EasyMock을 사용하여 Java 8의 java.time API에서 특정 날짜로 시간을 조롱하는 방법을 생각해 냈습니다.

  • Joda Time API없이
  • PowerMock없이.

수행해야 할 작업은 다음과 같습니다.

테스트 된 수업에서해야 할 일

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현재 날짜-시간을 호출하는 메소드에 새 속성 을 삽입하십시오 . 예를 들어, 제 경우에는 데이터베이스에 저장된 날짜가 이전 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;  // Be this a specific 
  private int month = 2;    // date we need 
  private int day = 3;      // to simulate.

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


0

이 예제는 Instant와 LocalTime을 결합하는 방법을 보여줍니다 ( 변환 문제에 대한 자세한 설명 ).

테스트중인 클래스

import java.time.Clock;
import java.time.LocalTime;

public class TimeMachine {

    private LocalTime from = LocalTime.MIDNIGHT;

    private LocalTime until = LocalTime.of(6, 0);

    private Clock clock = Clock.systemDefaultZone();

    public boolean isInInterval() {

        LocalTime now = LocalTime.now(clock);

        return now.isAfter(from) && now.isBefore(until);
    }

}

Groovy 테스트

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

import java.time.Clock
import java.time.Instant

import static java.time.ZoneOffset.UTC
import static org.junit.runners.Parameterized.Parameters

@RunWith(Parameterized)
class TimeMachineTest {

    @Parameters(name = "{0} - {2}")
    static data() {
        [
            ["01:22:00", true,  "in interval"],
            ["23:59:59", false, "before"],
            ["06:01:00", false, "after"],
        ]*.toArray()
    }

    String time
    boolean expected

    TimeMachineTest(String time, boolean expected, String testName) {
        this.time = time
        this.expected = expected
    }

    @Test
    void test() {
        TimeMachine timeMachine = new TimeMachine()
        timeMachine.clock = Clock.fixed(Instant.parse("2010-01-01T${time}Z"), UTC)
        def result = timeMachine.isInInterval()
        assert result == expected
    }

}

0

스프링 부트 테스트를위한 PowerMockito의 도움으로 ZonedDateTime. 다음이 필요합니다.

주석

테스트 클래스에 해당 서비스 준비해야 하여 사용합니다 ZonedDateTime.

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({EscalationService.class})
@SpringBootTest
public class TestEscalationCases {
  @Autowired
  private EscalationService escalationService;
  //...
}

테스트 케이스

테스트에서 원하는 시간을 준비하고 메서드 호출에 대한 응답으로 얻을 수 있습니다.

  @Test
  public void escalateOnMondayAt14() throws Exception {
    ZonedDateTime preparedTime = ZonedDateTime.now();
    preparedTime = preparedTime.with(DayOfWeek.MONDAY);
    preparedTime = preparedTime.withHour(14);
    PowerMockito.mockStatic(ZonedDateTime.class);
    PowerMockito.when(ZonedDateTime.now(ArgumentMatchers.any(ZoneId.class))).thenReturn(preparedTime);
    // ... Assertions 
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.