Zeitabhängige Unit-Tests

75

Ich muss eine Funktion testen, deren Ergebnis von der aktuellen Zeit abhängt (unter Verwendung der Joda-Zeit isBeforeNow()passiert dies).

public boolean isAvailable() {
    return (this.someDate.isBeforeNow());
}

Ist es möglich, die Systemzeit mit (z. B. mit Mockito) zu stummeln, um die Funktion zuverlässig zu testen?

Pojo
quelle
2
Für einige Funktionen besteht die einfachste Lösung darin, die aktuelle Zeit als Parameter zu übergeben.
DariusL
Bis Sie plötzlich einen fehlgeschlagenen Unit-Test wegen Sommerzeit bekommen;)
Pojo
1
Genau wie beim Verspotten muss es nicht die aktuelle Zeit sein. Sie können einen sicheren Zeitpunkt sofort fest codieren.
DariusL

Antworten:

64

Die Joda-Zeit unterstützt das Festlegen einer "falschen" aktuellen Zeit über die Methoden setCurrentMillisFixedund setCurrentMillisOffsetder DateTimeUtilsKlasse.

Siehe https://www.joda.org/joda-time/apidocs/org/joda/time/DateTimeUtils.html

Laurent Pireyn
quelle
11
Dies sind jedoch statische Methoden. Auf diese Weise führen Sie Abhängigkeiten zwischen den Unittests ein. Daher würde ich Jon Skeets Lösung vorziehen.
Hans-Peter Störr
hstoerr: Ich sehe keine Abhängigkeiten zwischen Tests, es sei denn, sie würden in verschiedenen Threads ausgeführt (was hier wahrscheinlich nicht der Fall ist). Aber selbst dann bietet Joda Time die DateTimeUtils.setCurrentMillisProvider(DateTimeUtils.MillisProvider)Methode, die sicherlich eine threadgebundene Implementierung ermöglichen würde.
Rogério
128

Der beste Weg (IMO), Ihren Code testbar zu machen, besteht darin, die Abhängigkeit von "Was ist die aktuelle Zeit?" In eine eigene Schnittstelle zu extrahieren. Dabei wird eine Implementierung verwendet, die die aktuelle Systemzeit verwendet (normalerweise verwendet), und eine Implementierung, mit der Sie die Zeit einstellen können , schieben Sie es vor, wie Sie wollen usw.

Ich habe diesen Ansatz in verschiedenen Situationen verwendet und er hat gut funktioniert. Es ist einfach einzurichten - erstellen Sie einfach eine Schnittstelle (z. B. Clock), die über eine einzige Methode verfügt, um den aktuellen Zeitpunkt in einem beliebigen Format anzuzeigen (z. B. mithilfe von Joda Time oder möglicherweise a Date).

Jon Skeet
quelle
7
Joda Time hat Unterstützung für diese Abstraktion eingebaut (siehe meine Antwort), sodass Sie sie nicht in Ihren Code einfügen müssen.
Laurent Pireyn
21
@Laurent: Ich finde das eigentlich nicht annähernd so elegant. Grundsätzlich denke ich , ein Dienst wie „die aktuelle Zeit immer“ ist eine Abhängigkeit (so wie ich Erzeugung von Zufallszahlen als Abhängigkeit sehen) , damit ich es ist gut , zu denken, dass explizit zu machen. Dies bedeutet, dass Sie auch Tests usw. parallelisieren können.
Jon Skeet
5
Punkt genommen. Versteh mich nicht falsch: Ich bin normalerweise für Abstraktion. Es kann jedoch schwierig sein, den Begriff der aktuellen Zeit in einem realen Projekt zu abstrahieren, insbesondere wenn Bibliotheken von Drittanbietern verwendet werden, die diesen Begriff nicht abstrahieren.
Laurent Pireyn
3
@Laurent: Oh ja, wenn Sie Bibliotheken von Drittanbietern verwenden, ist es schwierig ... aber dann hätten Sie das gleiche Problem mit setCurrentMillisFixed, es sei denn, Ihre Bibliothek von Drittanbietern verwendet zufällig Joda Time :(
Jon Skeet
3
@ Jon: Ich stimme dir vollkommen zu. Jetzt hat Java 8 die abstrakte Klasse Clock.
xli
22

Java 8 hat die abstrakte Klasse eingeführt java.time.Clock, mit der Sie eine alternative Implementierung zum Testen haben können. Genau das schlug Jon damals in seiner Antwort vor.

xli
quelle
1
Wenn Sie nur die Möglichkeit haben, nach der aktuellen Uhrzeit zu fragen, benötigen Sie keine ganze Uhr. Möglicherweise bevorzugen Sie stattdessen eine Supplierder interessierenden Zeiteinheiten (z LocalDateTime. B. ).
jub0bs
1
@Jubobs Sie können die aktuelle Zeit, die von zurückgegeben wird, nicht steuern, LocalDateTime.now()ohne Clockals Parameter übergeben zu werden now().
Deamon
1
@deamon Ich schlage vor, einen Typ-Port zu haben Supplier<LocalDateTime>, an den Sie entweder einen echten Adapter anschließen können - z. B. LocalDateTime::noweinen gefälschten Adapter - z. B. - () -> LocalDateTime.of(2017, 10, 24, 0, 0)wo es zweckmäßig ist (Komponententests). Keine Notwendigkeit für ein Ganzes Clock.
Jub0bs
4

Um Jon Skeets Antwort zu ergänzen, enthält Joda Time bereits eine aktuelle Zeitschnittstelle : DateTimeUtils.MillisProvider

Zum Beispiel:

import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils.MillisProvider;

public class Check {
    private final MillisProvider millisProvider;
    private final DateTime someDate;

    public Check(MillisProvider millisProvider, DateTime someDate) {
        this.millisProvider = millisProvider;
        this.someDate = someDate;
    }

    public boolean isAvailable() {
        long now = millisProvider.getMillis();
        return (someDate.isBefore(now));
    }
}

Verspotten Sie die Zeit in einem Unit-Test (mit Mockito, aber Sie können Ihre eigene Klasse MillisProviderMock implementieren):

DateTime fakeNow = new DateTime(2016, DateTimeConstants.MARCH, 28, 9, 10);
MillisProvider mockMillisProvider = mock(MillisProvider.class);
when(mockMillisProvider.getMillis()).thenReturn(fakeNow.getMillis());

Check check = new Check(mockMillisProvider, someDate);

Verwenden Sie die aktuelle Zeit in der Produktion ( DateTimeUtils.SYSTEM_MILLIS_PROVIDER wurde in 2.9.3 zu Joda Time hinzugefügt):

Check check = new Check(DateTimeUtils.SYSTEM_MILLIS_PROVIDER, someDate);
Arend v. Reinersdorff
quelle
1

Ich verwende einen ähnlichen Ansatz wie Jon, aber anstatt nur für die aktuelle Zeit Clockeine spezielle Schnittstelle zu erstellen (z. B. ), erstelle ich normalerweise eine spezielle Testschnittstelle (z. B. MockupFactory). Ich habe dort alle Methoden angegeben, die ich zum Testen des Codes benötige. In einem meiner Projekte habe ich dort beispielsweise vier Methoden:

  • eine, die einen Modelldatenbank-Client zurückgibt;
  • eine, die ein Mock-up-Notifier-Objekt erstellt, das den Code über Änderungen in der Datenbank benachrichtigt;
  • eine, die ein Modell java.util.Timer erstellt, das die Aufgaben ausführt, wenn ich es möchte;
  • eine, die die aktuelle Zeit zurückgibt.

Die getestete Klasse verfügt über einen Konstruktor, der diese Schnittstelle unter anderem akzeptiert. Die ohne dieses Argument erstellt nur eine Standardinstanz dieser Schnittstelle, die "im wirklichen Leben" funktioniert. Sowohl die Schnittstelle als auch der Konstruktor sind paketprivat, sodass die Test-API nicht außerhalb des Pakets ausläuft.

Wenn ich mehr nachgeahmte Objekte benötige, füge ich dieser Schnittstelle einfach eine Methode hinzu und implementiere sie sowohl in Test- als auch in realen Implementierungen.

Auf diese Weise entwerfe ich Code, der in erster Linie zum Testen geeignet ist, ohne dem Code selbst zu viel aufzuerlegen. Tatsächlich wird der Code auf diese Weise noch sauberer, da viel Fabrikcode an einem Ort gesammelt wird. Wenn ich beispielsweise in realem Code zu einer anderen Datenbankclient-Implementierung wechseln muss, muss ich nur eine Zeile ändern, anstatt nach Verweisen auf den Konstruktor zu suchen.

Natürlich funktioniert es genau wie bei Jons Ansatz nicht mit Code von Drittanbietern, den Sie nicht ändern können oder dürfen.

Sergei Tachenov
quelle
Das klingt so, als würden Sie eine neue Schnittstelle erstellen, um die Abhängigkeiten jeder getesteten Klasse zu kapseln. Was dann mindestens eine Implementierung erfordert. Nun haben Sie für jede zu testende Klasse die Klasse, mindestens eine Testklasse, die Abhängigkeitsgruppierungsschnittstelle und eine Implementierung dieser Schnittstelle? Das mag ich wirklich nicht. Und ich möchte hinzufügen, dass das Hinzufügen dieser zusätzlichen Indirektionsebene zwischen Ihrem Code und seinen tatsächlichen Abhängigkeiten (dh nachdem ich mir Ihre Klasse angesehen habe, muss ich mir dann auch die Abhängigkeitsschnittstelle ansehen) das Verständnis des Codes tatsächlich erschwert.
Dathan
@ Nathan, nein, nicht jede Klasse. Nur solche mit Abhängigkeiten, die beim Testen emuliert werden müssen. In meiner Bewerbung gibt es zufällig nur eine solche Klasse. Auch die Anzahl der Klassen hat keine Bedeutung. Wenn die Implementierung nur so class DefaultMockupFactory implements MockupFactory {Timer createTimer() {return new Timer();}}komplex ist, nicht wahr? Und factory.createTimer()irgendwo im Code zu sein, macht es auch nicht schwieriger, den Code zu verstehen. Aber ich stimme zu, dass dies in einigen Fällen möglicherweise nicht der beste Weg ist, dies zu tun.
Sergei Tachenov
Nein, ich denke nicht, dass es zu viel Komplexität hinzufügt - nur unnötige Komplexität. Ich bin der Meinung, dass die zusätzliche Indirektionsebene, die durch diese Fassadenschnittstelle erreicht wird, die Lesbarkeit des Codes beeinträchtigen kann. Wenn ich zum Beispiel Ihr Codebeispiel hatte, aber MockupFactoryeinige andere Methoden darauf hatte und alle Stellen im Code finden wollte, an denen createTimer()es verwendet wurde, muss ich von der zu testenden Klasse zur Benutzeroberfläche navigieren und dann suchen für die Verwendung der Methode, anstatt nur in der Klasse zu suchen.
Dathan
@Dathan, die Schnittstelle ist verschachtelt (mit paketprivater Sichtbarkeit), sodass sich alles noch in der Klasse befindet. Der Unterschied zwischen meinem und Jons Ansatz besteht darin, dass ich MockupFactoryfür alle Abhängigkeiten, die emuliert werden müssen, eine einzige Schnittstelle ( ) verwende, während Jon vorschlägt, für jede Abhängigkeit ( TimerFactoryund so weiter) eine separate Schnittstelle zu haben . Wie Sie den Code ohne Schnittstelle testen würden, ist mir ein Rätsel. Auf die eine oder andere Weise wird die zusätzliche Komplexität benötigt.
Sergei Tachenov
1
Ich stimme zu, einige Schnittstellen müssen hinzugefügt werden. Aber ich würde viel lieber der Schnittstellentrennung folgen - in diesem Fall würde ich Jons Ansatz stark bevorzugen. Ein Teil davon ist, dass viele Schnittstellen im System wiederverwendet werden - es ist wahrscheinlich, dass TimerFactorysie wiederverwendet werden. Daher können Sie sie einmal in einem DI-Container verkabeln und überall dieselbe verwenden, während dies MockupFactoryfür eine bestimmte Klasse unwahrscheinlich ist mehr Orte verwendet werden - das bedeutet, dass im Vergleich zu gut getrennten Schnittstellen mehr Konfiguration erforderlich ist.
Dathan