Java 8 的 java.time API 中的模拟时间

IT小君   2021-10-14T23:20:33
评论(11)
IT小君

最接近的东西是Clock物体。您可以使用任何时间(或从系统当前时间)创建时钟对象。所有 date.time 对象都有重载的now方法,这些方法采用时钟对象代替当前时间。所以你可以使用依赖注入来注入具有特定时间的时钟:

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

有关更多详细信息,请参阅时钟 JavaDoc

2021-10-14T23:20:33   回复
IT小君

我使用了一个新类来隐藏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();

    ...
}
2021-10-14T23:20:33   回复
IT小君

我用了一个字段

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)));
2021-10-14T23:20:33   回复
IT小君

我发现使用Clock混乱您的生产代码。

您可以使用JMockitPowerMock来模拟测试代码中的静态方法调用。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 对类似问题的回答的评论后,我不同意我过去的自我。最重要的是,这个论点让我相信,当你模拟静态方法时,你不能并行化测试。

但是,如果您必须处理遗留代码,您仍然可以/必须使用静态模拟。

2021-10-14T23:20:33   回复
IT小君

有点晚了,但这里是我使用java.dateKotlin 中API来模拟时间的方法

val now = LocalDate.of(2021, Month.FEBRUARY, 19)
val clock = Clock.fixed(Instant.ofEpochSecond(
    now.atStartOfDay().toEpochSecond(ZoneOffset.UTC)
), ZoneId.systemDefault())

然后你可以把你的时钟传给班级来测试

val classToTest = MyClass(clock)

当然,在您的可测试类中,您将使用时钟来检索日期或时间:

class MyClass(private val clock: Clock = Clock.systemDefaultZone()) {
    // ...
    fun doSomething() = LocalDate.now(clock)...
2021-10-14T23:20:33   回复
IT小君

我需要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()项目中。

因为它是spring boot应用程序。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

2021-10-14T23:20:33   回复
IT小君

这是使用 EasyMock 在 Java 8 Web 应用程序中将当前系统时间覆盖到特定日期以进行 JUnit 测试的工作方法

Joda Time 确实不错(谢谢 Stephen、Brian,你们让我们的世界变得更美好)但我不被允许使用它。

经过一些试验,我最终想出了一种使用 EasyMock 在 Java 8 的 java.time API 中模拟时间到特定日期的方法

  • 没有 Joda 时间 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()

2021-10-14T23:20:34   回复
IT小君

java.time.Clock与 mockito 依赖一起使用

testImplementation("org.mockito:mockito-core")
testImplementation("org.mockito:mockito-inline")

服务类使用Clock将在测试中模拟的字段。

@Service
public class DeliveryWithDateService {
    private final Clock clock = Clock.systemUTC();

    public Delivery plan(UUID orderId) {
        return Delivery.builder()
                .id(UUID.randomUUID())
                .orderId(orderId)
                .createdAt(ZonedDateTime.now(clock))
                .plannedAt(ZonedDateTime.now(clock)
                        .plusDays(1)
                        .withHour(8)
                        .truncatedTo(ChronoUnit.HOURS))
                .build();
    }

    public Delivery ship(Delivery delivery) {
        return Delivery.builder()
                .id(delivery.getId())
                .orderId(delivery.getOrderId())
                .createdAt(delivery.getCreatedAt())
                .shippedAt(ZonedDateTime.now(clock))
                .build();
    }
}

@Value
@Builder
public class Delivery {
    private UUID id;
    private UUID orderId;
    private ZonedDateTime createdAt;
    private ZonedDateTime plannedAt;
    private ZonedDateTime shippedAt;
}

单元测试使用Mockito.mockStatic来模拟Clock.

@SpringBootTest
public class DeliveryWithDateServiceTest {
    @Autowired
    private DeliveryWithDateService deliveryService;

    private static Clock clock;
    private static ZonedDateTime now;

    @BeforeAll
    static void setupClock() {
        clock = Clock.fixed(
                Instant.parse("2020-12-01T10:05:23.653Z"),
                ZoneId.of("Europe/Prague"));
        now = ZonedDateTime.now(clock);

        var clockMock = Mockito.mockStatic(Clock.class);
        clockMock.when(Clock::systemUTC).thenReturn(clock);
    }

    @Test
    void delivery_is_planned() {
        var orderId = UUID.randomUUID();
        var delivery = deliveryService.plan(orderId);

        var tomorrowAt8am = now.plusDays(1).withHour(8).truncatedTo(ChronoUnit.HOURS);

        assertAll(
                () -> assertThat(delivery).isNotNull(),
                () -> assertThat(delivery.getId()).isNotNull(),
                () -> assertThat(delivery.getOrderId()).isEqualTo(orderId),
                () -> assertThat(delivery.getCreatedAt()).isEqualTo(now),
                () -> assertThat(delivery.getPlannedAt()).isEqualTo(tomorrowAt8am),
                () -> assertThat(delivery.getShippedAt()).isNull()
        );
    }

    @Test
    void delivery_is_shipped() {
        var delivery = deliveryService.plan(UUID.randomUUID());
        var shipped = deliveryService.ship(delivery);
        assertAll(
                () -> assertThat(shipped).isNotNull(),
                () -> assertThat(shipped.getId()).isEqualTo(delivery.getId()),
                () -> assertThat(shipped.getOrderId()).isEqualTo(delivery.getOrderId()),
                () -> assertThat(shipped.getCreatedAt()).isEqualTo(delivery.getCreatedAt()),
                () -> assertThat(shipped.getShippedAt()).isEqualTo(now)
        );
    }
}
2021-10-14T23:20:34   回复
IT小君

这个例子甚至展示了如何结合 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
    }

}
2021-10-14T23:20:34   回复
IT小君

借助 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 
}
2021-10-14T23:20:34   回复
IT小君

使用 jmockit:

代码:

// Mocking time as 9am
final String mockTime = "09:00:00"
new MockUp<LocalTime>() {
       @Mock
       public LocalTime now() {
           return LocalTime.parse(mockTime);
       }
};

进口:

import mockit.MockUp;
import mockit.Mock;

依赖:

<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.41</version>
2021-10-14T23:20:34   回复