2014-11-21 7 views
36

Java 8 eingeführt java.time.Clock, die als Argument für viele andere java.time Objekte verwendet werden können, so dass Sie eine echte oder gefälschte Uhr in sie injizieren können. Zum Beispiel, ich weiß, dass Sie eine Clock.fixed() erstellen und dann Instant.now(clock) anrufen können und es wird die feste Instant zurückgeben, die Sie zur Verfügung gestellt haben. Das klingt perfekt für Unit-Tests!Einheit testet eine Klasse mit einem Java 8 Uhr

Allerdings habe ich Probleme herauszufinden, wie man das am besten nutzt. Ich habe eine Klasse, ähnlich der folgenden:

public class MyClass { 
    private Clock clock = Clock.systemUTC(); 

    public void method1() { 
     Instant now = Instant.now(clock); 
     // Do something with 'now' 
    } 
} 

Jetzt möchte ich diesen Code Einheit testen. Ich muss clock einstellen können, um feste Zeiten zu produzieren, damit ich method() zu verschiedenen Zeiten testen kann. Natürlich könnte ich Reflektion verwenden, um das Element clock auf bestimmte Werte zu setzen, aber es wäre schön, wenn ich nicht auf Reflexion zurückgreifen müsste. Ich könnte eine öffentliche setClock() Methode erstellen, aber das fühlt sich falsch an. Ich möchte kein Argument Clock zu der Methode hinzufügen, da der echte Code nicht mit dem Übergeben einer Uhr betroffen sein sollte.

Was ist der beste Ansatz für die Handhabung? Das ist neuer Code, damit ich die Klasse reorganisieren kann.

Edit: Um zu verdeutlichen, muss ich in der Lage sein, ein einzelnes MyClass Objekt zu konstruieren, aber in der Lage zu sein, dass ein Objekt zwei verschiedene Uhrwerte sehen kann (als ob es eine reguläre Systemuhr wäre). Daher kann ich keine feste Uhr in den Konstruktor übergeben.

+0

* Nun möchte ich diesen Code testen * Sie sollten angeben, welches Verhalten Sie von 'MyClass' erwarten. Das würde den Ansatz dazu bestimmen, hier zu folgen. – Jubobs

+0

Im Anschluss daran: Ich denke, es kommt im Grunde darauf an, dass man den "Clock.fixed" für Unit-Tests nicht so verwenden kann, wie ich es mir erhofft habe. Normale Spottansätze sind erforderlich. – Mike

Antwort

28

Lassen Sie mich Jon Skeet Antwort setzen und die Kommentare in den Code ein:

Klasse im Test:

public class Foo { 
    private final Clock clock; 
    public Foo(Clock clock) { 
     this.clock = clock; 
    } 

    public void someMethod() { 
     Instant now = clock.instant(); // this is changed to make test easier 
     System.out.println(now); // Do something with 'now' 
    } 
} 

Unit-Test:

public class FooTest() { 

    private Foo foo; 
    private Clock mock; 

    @Before 
    public void setUp() { 
     mock = mock(Clock.class); 
     foo = new Foo(mock); 
    } 

    @Test 
    public void ensureDifferentValuesWhenMockIsCalled() { 
     Instant first = Instant.now();     // e.g. 12:00:00 
     Instant second = first.plusSeconds(1);   // 12:00:01 
     Instant thirdAndAfter = second.plusSeconds(1); // 12:00:02 

     when(mock.instant()).thenReturn(first, second, thirdAndAfter); 

     foo.someMethod(); // string of first 
     foo.someMethod(); // string of second 
     foo.someMethod(); // string of thirdAndAfter 
     foo.someMethod(); // string of thirdAndAfter 
    } 
} 
+1

möchten Sie möglicherweise "first", "second" usw. nach Ihren Bedürfnissen ändern (z. B. die Zeit muss in der Vergangenheit sein), aber Sie bekommen die Idee. –

+12

In der Praxis wird der obige Beispieltest in NPEs laufen, weil der Benutzercode höchstwahrscheinlich 'LocalDateTime.now (clock)' 'anstatt' clock.instant() '; Eine "NullPointerException" wird ausgelöst, sobald 'LocalDateTime'' clock.getZone(). getRules() 'aufruft. Stattdessen sollte ein * real * Clock-Objekt mit einem Aufruf von "Clock.fixed (Instant, ZoneId)" erstellt werden. –

+0

@ Rogério meine Interpretation der Frage ist, "wie spotte ich, um verschiedene Werte auf aufeinanderfolgende Anrufe zurückgeben", so die Antwort. Ich weiß nicht genug über Java 8-Zeit für den 'LocalDateTime.now()' oder 'Instant.now()' Teil, aber wenn hier eine statische Methode benötigt wird, muss sie möglicherweise in eine Klasse eingebunden werden, da PowerMock/EasyMock [unterstützt nicht] (http://easymock.org/api/org/easymock/IExpectationSetters.html#andReturn-T-) 'Stubbing aufeinanderfolgende Anrufe' in [Mockito] (http://mockito.googlecode.com/ svn/tags/1.8.5/javadoc/org/mockito/Mockito.html # 10) –

32

Ich möchte kein Clock-Argument zu der Methode hinzufügen, weil der echte Code nicht mit dem Übergeben einer Uhr befassen sollte.

Nein ... aber man könnte es als Konstruktor Parameter zu betrachten. Im Grunde sagst du, dass deine Klasse eine Uhr braucht, mit der man arbeiten kann ... also ist das eine Abhängigkeit. Behandle sie wie jede andere Abhängigkeit und injiziere sie entweder in einem Konstruktor oder über eine Methode. (Ich persönlich bevorzuge Konstruktor Injektion, aber YMMV.)

Sobald Sie aufhören, es als etwas zu denken, können Sie sich leicht konstruieren, und fangen an, es als "nur eine andere Abhängigkeit" zu denken, dann können Sie vertraute Techniken verwenden. (Ich nehme an, Sie sind mit der Abhängigkeitsinjektion im Allgemeinen wohl, zugegeben.)

+0

Ja, ich dachte darüber nach. Das Problem besteht jedoch darin, dass ich durch das Übergeben einer Uhr durch den Konstruktor eine einzelne feste Uhr einstellen kann. Ich denke, ich habe es nicht klargestellt, aber ich muss in der Lage sein, die Uhr in einem einzigen Test auf mehrere Werte zu setzen, ohne ein neues Objekt zu konstruieren. – Mike

+1

@Mike Bitte aktualisieren Sie Ihre Probe, um klar zu machen, was Ihr Anwendungsfall ist. – Puce

+0

@Mike Dann haben Sie entweder eine Uhr Setter, Mock "Clock", oder verwenden Sie Reflexion. Welche anderen Optionen sehen Sie basierend auf Ihren Anforderungen? –

-1

Sie geben entweder MyClass eine Uhr, Schein Clock, orretrieve eine Uhr – gibt es nicht viel mehr Optionen.

Sie müssen nicht die Reflexion selbst tun, verwenden Sie eine Mocking-Bibliothek.

Meinungen variieren, was der "richtige" Ansatz ist.

15

Ich bin ein bisschen spät zum Spiel hier, aber zu den anderen Antworten hinzuzufügen, die vorschlagen, eine Uhr zu verwenden - das funktioniert definitiv, und von uns Mit Mockitos DoAnswer können Sie eine Uhr erstellen, die Sie dynamisch an Ihre Fortschritte anpassen können.

Nehmen Sie diese Klasse an, die geändert wurde, um eine Uhr im Konstruktor zu nehmen, und auf die Uhr auf Instant.now(clock) Aufrufe verweisen.

public class TimePrinter() { 
    private final Clock clock; // init in constructor 

    // ... 

    public void printTheTime() { 
     System.out.println(Instant.now(clock)); 
    } 
} 

Dann in Ihrem Testaufbau:

private Instant currentTime; 
private TimePrinter timePrinter; 

public void setup() { 
    currentTime = Instant.EPOCH; // or Instant.now() or whatever 

    // create a mock clock which returns currentTime 
    final Clock clock = mock(Clock.class); 
    when(clock.instant()).doAnswer((invocation) -> currentTime); 

    timePrinter = new TimePrinter(clock); 
} 

Später im Test:

@Test 
public void myTest() { 
    myObjectUnderTest.printTheTime(); // 1970-01-01T00:00:00Z 

    // go forward in time a year 
    currentTime = currentTime.plus(1, ChronoUnit.YEARS); 

    myObjectUnderTest.printTheTime(); // 1971-01-01T00:00:00Z 
} 

Sie sagen Mockito immer eine Funktion ausführen, die den aktuellen Wert von current zurück Wann immer sofort() aufgerufen wird. Instant.now(clock) wird clock.instant() anrufen. Jetzt können Sie schneller vorspulen, zurückspulen und im Allgemeinen Zeitreisen besser als ein DeLorean.

+0

Diese Antwort ist perfekt. –

+0

Mein Anwendungsfall besteht darin, bei einigen Tests einige Sekunden vorzuspulen, um einige zeitbasierte Validierungsregeln zu überprüfen. Dies scheint mir die einfachste und eleganteste Lösung zu sein. Perfekt, vielen Dank. Noch ein +1 für den DeLorean. –

+0

Wirklich elegante Lösung :) +1 für die DeLorean Referenz. – Bohsen

Verwandte Themen