Verspottungszeit in der java.time-API von Java 8


Antworten:


71

Das nächste ist das ClockObjekt. Sie können ein Clock-Objekt jederzeit (oder ab der aktuellen Systemzeit) erstellen. Alle date.time-Objekte verfügen über überladene nowMethoden, die stattdessen ein Uhrobjekt für die aktuelle Uhrzeit verwenden. Sie können also die Abhängigkeitsinjektion verwenden, um eine Uhr mit einer bestimmten Zeit zu injizieren:

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

Siehe Clock JavaDoc für weitere Details


11
Ja. Dies ist insbesondere Clock.fixedbeim Testen nützlich, während Clock.systemoder Clock.systemUTCmöglicherweise in der Anwendung verwendet wird.
Matt Johnson-Pint

8
Schade, dass es keine veränderbare Uhr gibt, mit der ich sie auf nicht tickende Zeit einstellen kann, aber diese Zeit später ändern kann (Sie können dies mit joda tun). Dies ist nützlich, um zeitkritischen Code zu testen, z. B. einen Cache mit zeitbasierten Ablaufzeiten oder eine Klasse, die zukünftige Ereignisse plant.
Bacar

2
@ Bacar Clock ist abstrakte Klasse, Sie könnten Ihre eigene Test Clock Implementierung erstellen
Bjarne Boström

Ich glaube, das haben wir letztendlich getan.
Bacar

22

Ich habe eine neue Klasse verwendet, um die Clock.fixedErstellung auszublenden und die Tests zu vereinfachen:

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
Was ist mit der Thread-Sicherheit, wenn mehrere Tests parallel ausgeführt werden und die TimeMachine-Uhr ändern?
Youri

Sie müssen die Uhr an das getestete Objekt übergeben und beim Aufrufen zeitbezogener Methoden verwenden. Sie können die getClock()Methode auch entfernen und das Feld direkt verwenden. Diese Methode fügt nur ein paar Codezeilen hinzu.
Deamon

1
Banktime oder TimeMachine?
Emanuele

9

Ich habe ein Feld benutzt

private Clock clock;

und dann

LocalDate.now(clock);

in meinem Produktionscode. Dann habe ich Mockito in meinen Unit-Tests verwendet, um die Uhr mit Clock.fixed () zu verspotten:

@Mock
private Clock clock;
private Clock fixedClock;

Verspottung:

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

Behauptung:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));

8

Ich finde mit ClockUnordnung Ihren Produktionscode.

Sie können JMockit oder PowerMock verwenden , um statische Methodenaufrufe in Ihrem Testcode zu verspotten. Beispiel mit 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 : Nachdem ich die Kommentare zu Jon Skeets Antwort auf eine ähnliche Frage hier auf SO gelesen habe, bin ich nicht mit meinem früheren Ich einverstanden. Das Argument hat mich vor allem davon überzeugt, dass Sie Tests nicht paralleln können, wenn Sie statische Methoden verspotten.

Sie können / müssen jedoch statisches Mocking verwenden, wenn Sie sich mit Legacy-Code befassen müssen.


1
+1 für den Kommentar "Statisches Verspotten für Legacy-Code verwenden". Für neuen Code sollten Sie sich daher an Dependency Injection anlehnen und eine Uhr einspeisen (feste Uhr für Tests, Systemuhr für Produktionslaufzeit).
David Groomes

1

Ich brauche LocalDateInstanz statt LocalDateTime.
Aus diesem Grund habe ich folgende Dienstprogrammklasse erstellt:

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

Und die Verwendung dafür ist wie folgt:

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

Ausgabe:

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

Ersetzt die gesamte Schöpfung LocalDate.now()bis Clock.getCurrentDate()in Projekt.

Weil es eine Spring Boot- Anwendung ist. Vor dem testProfil Ausführung gesetzt nur einen vordefinierten Zeitpunkt für alle 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);
        }
    }
}

Und zu spring.factories hinzufügen :

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


1

Hier erfahren Sie, wie Sie die aktuelle Systemzeit für JUnit-Testzwecke in einer Java 8-Webanwendung mit EasyMock auf ein bestimmtes Datum überschreiben können

Joda Time ist sicher schön (danke Stephen, Brian, du hast unsere Welt zu einem besseren Ort gemacht), aber ich durfte sie nicht benutzen.

Nach einigen Experimenten fand ich schließlich eine Möglichkeit, die Zeit bis zu einem bestimmten Datum in der java.time-API von Java 8 mit EasyMock zu verspotten

  • Ohne Joda Time API
  • Ohne PowerMock.

Folgendes muss getan werden:

Was ist in der getesteten Klasse zu tun?

Schritt 1

Fügen Sie java.time.Clockder getesteten Klasse ein neues Attribut hinzu MyServiceund stellen Sie sicher, dass das neue Attribut bei Standardwerten mit einem Instanziierungsblock oder einem Konstruktor ordnungsgemäß initialisiert wird:

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

Schritt 2

Fügen Sie das neue Attribut clockin die Methode ein, die ein aktuelles Datum und eine aktuelle Uhrzeit anfordert. In meinem Fall musste ich beispielsweise überprüfen, ob ein in der Datenbank gespeichertes Datum zuvor aufgetreten ist LocalDateTime.now(), das ich LocalDateTime.now(clock)wie folgt ersetzt habe:

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

Was ist in der Testklasse zu tun?

Schritt 3

Erstellen Sie in der Testklasse ein Mock-Clock-Objekt, fügen Sie es in die Instanz der getesteten Klasse ein, bevor Sie die getestete Methode aufrufen doExecute(), und setzen Sie es anschließend wie folgt zurück:

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

Überprüfen Sie dies im Debug-Modus, und Sie werden sehen, dass das Datum vom 3. Februar 2017 korrekt in die myServiceInstanz eingefügt und in der Vergleichsanweisung verwendet wurde und dann ordnungsgemäß auf das aktuelle Datum mit zurückgesetzt wurde initDefaultClock().


0

Dieses Beispiel zeigt sogar, wie Instant und LocalTime kombiniert werden ( detaillierte Erläuterung der Probleme bei der Konvertierung ).

Eine Klasse im 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);
    }

}

Ein 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

Mit Hilfe von PowerMockito für einen Spring Boot Test können Sie das verspotten ZonedDateTime. Sie benötigen Folgendes.

Anmerkungen

In der Testklasse müssen Sie den Dienst vorbereiten, der das verwendet ZonedDateTime.

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

Testfall

Im Test können Sie eine gewünschte Zeit vorbereiten und als Antwort auf den Methodenaufruf abrufen.

  @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 
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.