เวลาจำลองใน java.time API ของ Java 8


คำตอบ:


73

สิ่งที่ใกล้ตัวที่สุดคือClockวัตถุ คุณสามารถสร้างวัตถุนาฬิกาโดยใช้เวลาใดก็ได้ที่คุณต้องการ (หรือจากเวลาปัจจุบันของระบบ) อ็อบเจ็กต์ date.time ทั้งหมดมีnowเมธอดที่โอเวอร์โหลดซึ่งใช้อ็อบเจ็กต์นาฬิกาแทนเวลาปัจจุบัน ดังนั้นคุณสามารถใช้การฉีดพึ่งพาเพื่อฉีดนาฬิกาตามเวลาที่กำหนด:

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

ดูนาฬิกา JavaDocสำหรับรายละเอียดเพิ่มเติม


11
ใช่. โดยเฉพาะอย่างยิ่งClock.fixedมีประโยชน์ในการทดสอบในขณะที่Clock.systemหรือClock.systemUTCอาจใช้ในแอปพลิเคชัน
Matt Johnson-Pint

8
น่าเสียดายที่ไม่มีนาฬิกาที่เปลี่ยนแปลงไม่ได้ที่อนุญาตให้ฉันตั้งค่าเป็นเวลาที่ไม่ใช้เครื่องหมาย แต่จะแก้ไขเวลานั้นในภายหลัง (คุณสามารถทำได้ด้วย joda) สิ่งนี้จะเป็นประโยชน์สำหรับการทดสอบรหัสที่คำนึงถึงเวลาเช่นแคชที่มีการหมดอายุตามเวลาหรือคลาสที่กำหนดเวลาเหตุการณ์ในอนาคต
bacar

2
@bacar 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 ():

@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 สำหรับความคิดเห็น "using static mocking for legacy code" ดังนั้นสำหรับโค้ดใหม่ขอแนะนำให้ใช้ Dependency Injection และฉีด Clock (นาฬิกาคงที่สำหรับการทดสอบนาฬิการะบบสำหรับรันไทม์การผลิต)
David Groomes

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

นี่คือวิธีการทำงานเพื่อแทนที่เวลาของระบบปัจจุบันเป็นวันที่เฉพาะเจาะจงสำหรับวัตถุประสงค์ในการทดสอบ JUnit ในเว็บแอปพลิเคชัน Java 8 ด้วย EasyMock

Joda Time นั้นดีแน่นอน (ขอบคุณ Stephen, Brian คุณทำให้โลกของเราน่าอยู่ขึ้น) แต่ฉันไม่ได้รับอนุญาตให้ใช้

หลังจากการทดลองบางอย่างในที่สุดฉันก็หาวิธีจำลองเวลาไปยังวันที่ที่ระบุใน java.time API ของ Java 8 ด้วย EasyMock

  • ไม่มี 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(..)
    }
  }

ตรวจสอบในโหมดการแก้ปัญหาและคุณจะเห็นวันที่ 3 กุมภาพันธ์ 2017 ที่ได้รับการฉีดอย่างถูกต้องลงในอินสแตนซ์และนำมาใช้ในการเรียนการสอนการเปรียบเทียบและจากนั้นได้รับอย่างถูกต้องรีเซ็ตวันที่ปัจจุบันด้วยmyServiceinitDefaultClock()


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.