Java8的 Java.time API 中的模拟时间

Joda Time 有一个很好的 SetCurrentMillisfix ()来模拟时间。

这在测试中很实用。

Java8的 Java.time API中是否有一个等价物?

109665 次浏览

The closest thing is the Clock object. You can create a Clock object using any time you want (or from the System current time). All date.time objects have overloaded now methods that take a clock object instead for the current time. So you can use dependency injection to inject a Clock with a specific time:

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

See Clock JavaDoc for more details

I used a new class to hide the Clock.fixed creation and simplify the tests:

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


...
}

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.

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
}


}

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

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

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

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
}

A bit late, but here it is what I use to mock time using the java.date API in Kotlin:

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

and then you can pass your clock to the class to test

val classToTest = MyClass(clock)

Of course, inside your testable class you will use the clock to retrieve dates or times:

class MyClass(private val clock: Clock = Clock.systemDefaultZone()) {
// ...
fun doSomething() = LocalDate.now(clock)...

Use jmockit:

Code:

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

Imports:

import mockit.MockUp;
import mockit.Mock;

Dependency:

<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.41</version>

I used java.time.Clock with mockito dependency

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

The service class uses a Clock field which will be mocked on the test.

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


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


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


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

The unit test uses the Mockito.mockStatic to mock the Clock.

@SpringBootTest
public class TimeTestWithDateServiceTest {
@Autowired
private TimeTestWithDateService timeTestService;


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 timeTest_is_planned() {
var orderId = UUID.randomUUID();
var timeTest = timeTestService.plan(orderId);


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


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


@Test
void timeTest_is_shipped() {
var timeTest = timeTestService.plan(UUID.randomUUID());
var shipped = timeTestService.ship(timeTest);
assertAll(
() -> assertThat(shipped).isNotNull(),
() -> assertThat(shipped.getId()).isEqualTo(timeTest.getId()),
() -> assertThat(shipped.getOrderId()).isEqualTo(timeTest.getOrderId()),
() -> assertThat(shipped.getCreatedAt()).isEqualTo(timeTest.getCreatedAt()),
() -> assertThat(shipped.getShippedAt()).isEqualTo(now)
);
}
}

here is the answer : 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())
}
}