Recht häufig habe ich Klassen, die dauer- und zeitabhängige Zustände prüfen. Verwendet man System.currentTimeMillis()
, kann man in Testcases nur noch schwer mocken und ist auf Thread.sleep()
in den Testcases angewiesen.
Wer hat an der Uhr gedreht? #
Im Folgenden habe ich eine Klasse TimeUser
, welche die Systemzeit nach hält, um bei konsekutiven Aufrufen zu prüfen, ob seit dem ersten Aufruf eine Sekunde (=1000 Millisekunden) oder mehr vergangen ist.
Die Implementierung mit System.currentTimeMillis()
ist recht einfach:
public class TimeUser {
private long firstCallMilliseconds = 0L;
public boolean checkTime() {
final long now = System.currentTimeMillis();
if (firstCallMilliseconds == 0L) {
firstCallMilliseconds = now;
}
if (now - firstCallMilliseconds > 1000L) {
return true;
}
return false;
}
}
Um zu prüfen, ob dies funktioniert, schreiben wir einen passenden Testcase:
public class ClockMock {
private TimeUser timeUser;
@BeforeEach
public void setup() {
timeUser = new TimeUser();
}
@Test
public void checkTime_sleep() throws InterruptedException {
assertThat(timeUser.checkTime(), is(false));
Thread.sleep(1000L * 2L); // Sicherheitspuffer = x 2
assertThat(timeUser.checkTime(), is(true));
}
}
Das funktioniert ganz gut, hat aber den Nachteil, dass der Testcase zwei Sekunden schläft und damit die Ausführung verzögert.
Sinnvoller wäre es, wenn wir die Zeit im Test manipulieren könnten.
Fluxcapacitor #
Mit Powermock könnte man auch System.currentTimeMillis()
mocken, aber ich muss gestehen, dass ich Powermock nicht so sehr mag. Es ist sehr fragil und kann
schnell zu Nebeneffekten führen. Ich ziehe es daher vor, das Ganze durch die Umstrukturierung
der Klasse etwas testbarer zu machen.
Hierzu verwende ich die Java-eigene Klasse Clock
und Instant.
Über Instant.now(clock)
können wir die aktuelle Uhrzeit erfragen. Dabei kann die übergebene
Clock
genutzt werden, einen beliebigen Zeitgeber mitzugeben.
public class TimeUser {
private final Clock clock;
private long firstCallMilliseconds;
public TimeUser(Clock clock) {
this.clock = clock;
}
public boolean checkTime() {
final long now = Instant.now(clock).toEpochMilli();
if (firstCallMilliseconds == 0L) {
firstCallMilliseconds = now;
}
if ((now - firstCallMilliseconds) > 1000L) {
return true;
}
return false;
}
}
Nun kann ich im Testcase selber die Clock
mocken. Dazu muss man nur wissen,
das Instant
selber die Methode clock.instant()
aufrufen wird, um den aktuellen
Zeitpunkt zu bestimmen. Hier kann ich die von der Java-API zur Verfügung gestellte
Offset-Clock
verwenden, um die Zeit vor- oder zurückzudrehen.
import java.time.Instant;
public class ClockMock {
private Clock clock;
private TimeUser timeUser;
@BeforeEach
public void setup() {
clock = mock(Clock.class);
// Default-Verhalten: Echte Systemzeit
final Instant realtimeInstant = Clock.systemUTC().instant();
doReturn(realtimeInstant).when(clock).instant();
timeUser = new TimeUser(clock);
}
@Test
public void checkTime_clock() {
assertThat(timeUser.checkTime(), is(false));
// Zeitreise in die Zukunft: Echte Systemzeit + 2 Sekunden
final Instant futureInstant = Clock.offset(Clock.systemUTC(), Duration.ofMillis(1000L * 2L)).instant();
doReturn(futureInstant).when(clock).instant();
assertThat(timeUser.checkTime(), is(true));
}
}