~/home of geeks

Zeiten in Testcases mocken

· 429 Wörter · 3 Minute(n) Lesedauer

Portal verbindet verschiedene Welten

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