【问题标题】:Mocking time in Java 8's java.time APIJava 8 的 java.time API 中的模拟时间
【发布时间】:2014-06-30 13:29:18
【问题描述】:

Joda Time 有一个不错的 DateTimeUtils.setCurrentMillisFixed() 来模拟时间。

在测试中非常实用。

Java 8's java.time API 中是否有等价物?

【问题讨论】:

    标签: java datetime mocking java-8 java-time


    【解决方案1】:

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

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

    详情请见Clock JavaDoc

    【讨论】:

    • 是的。特别是,Clock.fixed 在测试中很有用,而Clock.systemClock.systemUTC 可能在应用程序中使用。
    • 很遗憾没有一个可变时钟允许我将其设置为非滴答时间,但稍后修改该时间(您可以使用 joda 执行此操作)。这对于测试时间敏感的代码很有用,例如具有基于时间的到期的缓存或安排未来事件的类。
    • @bacar Clock 是抽象类,你可以创建自己的测试时钟实现
    • 我相信这就是我们最终所做的。
    【解决方案2】:

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

    【讨论】:

    • 如果并行运行多个测试并更改 TimeMachine 时钟,线程安全会怎样?
    • 您必须将时钟传递给被测对象,并在调用时间相关方法时使用它。您可以删除 getClock() 方法并直接使用该字段。这个方法只添加了几行代码。
    • 银行时间还是时间机器?
    【解决方案3】:

    我使用了一个字段

    private Clock clock;
    

    然后

    LocalDate.now(clock);
    

    在我的生产代码中。然后我在单元测试中使用 Mockito 来模拟 Clock 使用 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)));
    

    【讨论】:

      【解决方案4】:

      我发现使用 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 对类似question here on SO 的回答后,我不同意我过去的自我。最重要的是,这个论点让我确信,当你模拟静态方法时,你不能使测试并行化。

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

      【讨论】:

      • +1 表示“对遗留代码使用静态模拟”评论。因此,对于新代码,鼓励使用依赖注入并注入一个时钟(用于测试的固定时钟,用于生产运行时的系统时钟)。
      【解决方案5】:

      有点晚了,但这是我在 Kotlin 中使用 java.date 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)...
      

      【讨论】:

      • 在不通过Clock 的情况下实施你怎么看?任何成本/收益?通过时钟有什么好处?例如,如果它是一个与时间无关的类
      • 所以,第一件事是:只有当你真正需要它时,你才传递Clock。我的意思是,如果你在课堂上没有得到任何日期/时间,你当然不需要通过Clock。鉴于此,通常目标是尝试拥有单一职责的类,因此只有使用基于日期的实现的类才会具有这种依赖关系。也就是说,我认为主要优势是每个需要日期/时间的课程都有一个共同的合同和实践。
      【解决方案6】:

      我需要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 profile 执行之前,只需为所有测试设置一个预定义的日期:

      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

      【讨论】:

        【解决方案7】:

        这是一种在 Java 8 Web 应用程序中使用 EasyMock 将当前系统时间覆盖为特定日期以进行 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
          // (...)
        }
        

        第二步

        将新属性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();
            }
          }
          // (...) 
        }
        

        测试类需要做什么

        第三步

        在测试类中,在调用测试方法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()

        【讨论】:

          【解决方案8】:

          我使用 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)
                  );
              }
          }
          

          【讨论】:

            【解决方案9】:

            这个例子甚至展示了如何结合 Instant 和 LocalTime (detailed explanation of issues with the conversion)

            一个正在测试的类

            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
                }
            
            }
            

            【讨论】:

              【解决方案10】:

              借助 PowerMockito 进行 Spring Boot 测试,您可以模拟 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 
              }
              

              【讨论】:

                【解决方案11】:

                使用 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>
                

                【讨论】:

                  【解决方案12】:

                  这是答案:https://gabstory.com/70?category=933660

                  import com.nhaarman.mockitokotlin2.given
                  import org.junit.jupiter.api.Assertions.assertEquals
                  import org.junit.jupiter.api.BeforeEach
                  import org.junit.jupiter.api.Test
                  import org.junit.jupiter.api.extension.ExtendWith
                  import org.mockito.Mock
                  import org.mockito.Mockito.mockStatic
                  import org.mockito.junit.jupiter.MockitoExtension
                  import org.springframework.data.projection.ProjectionFactory
                  import org.springframework.data.projection.SpelAwareProxyProjectionFactory
                  import java.time.Clock
                  import java.time.ZonedDateTime
                  
                  @ExtendWith(MockitoExtension::class)
                  class MyTest {
                     private val clock = Clock.fixed(ZonedDateTime.parse("2021-10-25T00:00:00.000+09:00[Asia/Seoul]").toInstant(), SEOUL_ZONE_ID)
                     
                      @BeforeEach
                      fun setup() {
                          runCatching {
                              val clockMock = mockStatic(Clock::class.java)
                              clockMock.`when`<Clock>(Clock::systemDefaultZone).thenReturn(clock)
                          }
                      }
                      
                      @Test
                      fun today(){
                        assertEquals("2021-10-25T00:00+09:00[Asia/Seoul]", ZonedDateTime.now().toString())
                      }
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2015-06-27
                    • 2014-11-02
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-11-08
                    • 2013-07-05
                    • 2015-09-30
                    • 2014-05-12
                    相关资源
                    最近更新 更多