Mocking time in Java 8's java.time API

后端 未结 8 1549
我寻月下人不归
我寻月下人不归 2020-11-27 16:09

Joda Time has a nice DateTimeUtils.setCurrentMillisFixed() to mock time.

It\'s very practical in tests.

Is there an equivalent in Java 8\'s java.time API

相关标签:
8条回答
  • 2020-11-27 16:44

    This example even shows how to combine Instant and LocalTime (detailed explanation of issues with the conversion)

    A class under test

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

    A Groovy test

    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 讨论(0)
  • 2020-11-27 16:50

    I need LocalDate instance instead of LocalDateTime.
    With such reason I created following utility class:

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

    And usage for it is like:

    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());
        }
    }
    

    Output:

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

    Replaced all creation LocalDate.now() to Clock.getCurrentDate() in project.

    Because it is spring boot application. Before test profile execution just set a predefined date for all tests:

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

    And add to spring.factories:

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

    0 讨论(0)
  • 2020-11-27 16:51

    I used a field

    private Clock clock;
    

    and then

    LocalDate.now(clock);
    

    in my production code. Then I used Mockito in my unit tests to mock the Clock using Clock.fixed():

    @Mock
    private Clock clock;
    private Clock fixedClock;
    

    Mocking:

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

    Assertion:

    assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));
    
    0 讨论(0)
  • 2020-11-27 16:51

    I find using Clock clutters your production code.

    You can use JMockit or PowerMock to mock static method invocations in your test code. Example with 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);
    }
    

    EDIT: After reading the comments on Jon Skeet's answer to a similar question here on SO I disagree with my past self. More than anything else the argument convinced me that you cannot parallize tests when you mock static methods.

    You can/must still use static mocking if you have to deal with legacy code, though.

    0 讨论(0)
  • 2020-11-27 16:51

    Here's a working way to override current system time to a specific date for JUnit testing purposes in a Java 8 web application with EasyMock

    Joda Time is sure nice (thank you Stephen, Brian, you've made our world a better place) but I wasn't allowed to use it.

    After some experimenting, I eventually came up with a way to mock time to a specific date in Java 8's java.time API with EasyMock

    • Without Joda Time API
    • Without PowerMock.

    Here's what needs to be done:

    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 database happened before LocalDateTime.now(), which I replaced 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;  // 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(..)
        }
      }
    

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

    0 讨论(0)
  • With the help of PowerMockito for a spring boot test you can mock the ZonedDateTime. You need the following.

    Annotations

    On the test class you need to prepare the service which uses the the ZonedDateTime.

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

    Test case

    In the test you can prepare a desired time, and get it in response of the method call.

      @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 
    }
    
    0 讨论(0)
提交回复
热议问题