Ist dies eine angemessene Anwendung der Reset-Methode von Mockito?

68

Ich habe eine private Methode in meiner Testklasse, die ein häufig verwendetes BarObjekt erstellt. Der BarKonstruktor ruft die someMethod()Methode in meinem verspotteten Objekt auf:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

In einigen meiner Testmethoden, die ich überprüfen möchte, someMethodwurde auch von diesem bestimmten Test aufgerufen. So etwas wie das folgende:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

Dies schlägt fehl, da das verspottete Objekt someMethodzweimal aufgerufen wurde. Ich möchte nicht, dass sich meine Testmethoden um die Nebenwirkungen meiner getBar()Methode kümmern. Wäre es also sinnvoll, mein Scheinobjekt am Ende von zurückzusetzen getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

Ich frage, weil die Dokumentation vorschlägt , dass das Zurücksetzen von Scheinobjekten im Allgemeinen auf schlechte Tests hindeutet. Dies fühlt sich für mich jedoch in Ordnung an.

Alternative

Die alternative Wahl scheint zu sein:

verify(mockedObject, times(2)).someMethod();

was meiner meinung nach jeden test zwingt, über die erwartungen von zu wissen getBar(), ohne gewinn.

Duncan Jones
quelle

Antworten:

60

Ich glaube, dies ist einer der Fälle, in denen die Verwendung reset()in Ordnung ist. Der Test, den Sie schreiben, testet, dass "einige Dinge" einen einzelnen Anruf auslösen someMethod(). Das Schreiben der verify()Anweisung mit einer anderen Anzahl von Aufrufen kann zu Verwirrung führen.

  • atLeastOnce() Dies ist eine schlechte Sache, da Sie möchten, dass Ihre Tests immer korrekt sind.
  • times(2)verhindert das falsche Positiv, lässt es jedoch so erscheinen, als würden Sie zwei Aufrufe erwarten, anstatt zu sagen, dass "ich weiß, dass der Konstruktor einen hinzufügt". Wenn sich im Konstruktor etwas ändert, um einen zusätzlichen Aufruf hinzuzufügen, hat der Test jetzt die Möglichkeit, ein falsches positives Ergebnis zu erzielen. Und das Entfernen des Anrufs würde dazu führen, dass der Test fehlschlägt, da der Test jetzt falsch ist, anstatt dass das, was gerade getestet wird, falsch ist.

Durch die Verwendung reset()in der Hilfsmethode vermeiden Sie diese beiden Probleme. Sie müssen jedoch darauf achten, dass auch die von Ihnen vorgenommenen Stubs zurückgesetzt werden. Seien Sie also gewarnt. Der Hauptgrund, von dem reset()abgeraten wird, ist zu verhindern

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

Dies ist nicht das, was das OP versucht. Ich nehme an, das OP verfügt über einen Test, der den Aufruf im Konstruktor überprüft. Für diesen Test ermöglicht das Zurücksetzen, diese einzelne Aktion und ihre Wirkung zu isolieren. Dies ist einer der wenigen Fälle, bei reset()denen hilfreich sein kann. Die anderen Optionen, die nicht alle verwenden, haben Nachteile. Die Tatsache, dass das OP diesen Beitrag verfasst hat, zeigt, dass er über die Situation nachdenkt und die Rücksetzmethode nicht nur blind anwendet.

unholysampler
quelle
17
Ich wünschte, Mockito hätte einen resetInteractions () -Aufruf bereitgestellt, um einfach die vergangenen Interaktionen zu vergessen, um zu überprüfen (..., times (...)) und das Stubbing beizubehalten. Dies würde die Testsituationen von {setup; Handlung; überprüfe;} das ist viel einfacher zu handhaben. Es wäre {setup; resetInteractions; Handlung; verify}
Arkadiy
2
Seit Mockito 2.1 gibt es tatsächlich eine Möglichkeit, Aufrufe zu löschen, ohne die Stubs zurückzusetzen:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

Smart Mockito-Benutzer verwenden die Rücksetzfunktion kaum, da sie wissen, dass dies ein Zeichen für schlechte Tests sein kann. Normalerweise müssen Sie Ihre Mocks nicht zurücksetzen, sondern erstellen für jede Testmethode neue Mocks.

Anstatt reset()zu überlegen, ob Sie einfache, kleine und fokussierte Testmethoden über lange, zu spezifizierte Tests schreiben möchten. Der erste mögliche Codegeruch befindet sich reset()in der Mitte der Testmethode.

Extrahiert aus den Mockito-Dokumenten .

Mein Rat ist, dass Sie versuchen, die Verwendung zu vermeiden reset(). Meiner Meinung nach sollte ein zweimaliger Aufruf von someMethod getestet werden (möglicherweise handelt es sich um einen Datenbankzugriff oder einen anderen langen Prozess, um den Sie sich kümmern möchten).

Wenn Ihnen das wirklich egal ist, können Sie Folgendes verwenden:

verify(mockedObject, atLeastOnce()).someMethod();

Beachten Sie, dass dies zu einem falschen Ergebnis führen kann, wenn Sie someMethod von getBar und nicht danach aufrufen (dies ist ein falsches Verhalten, aber der Test schlägt nicht fehl).

greuze
quelle
2
Ja, ich habe das genaue Zitat gesehen (ich habe es aus meiner Frage verlinkt). Momentan sehe ich noch kein anständiges Argument dafür, warum mein Beispiel oben "schlecht" ist. Können Sie einen liefern?
Duncan Jones
Wenn Sie Ihre Scheinobjekte zurücksetzen müssen, scheint es, als würden Sie in Ihrem Test zu viele Dinge testen. Sie können es in zwei Tests aufteilen, um kleinere Dinge zu testen. Auf jeden Fall weiß ich nicht, warum Sie in der getBar-Methode überprüfen, es ist schwierig zu verfolgen, was Sie testen. Ich empfehle Ihnen, Ihren Test so zu gestalten, wie es Ihre Klasse tun soll (wenn Sie someMethod genau zweimal aufrufen müssen, mindestens einmal, nur einmal, nie usw.), und jede Überprüfung an derselben Stelle durchzuführen.
Greuze
Ich habe meine Frage bearbeitet, um hervorzuheben, dass das Problem auch dann weiterhin besteht, wenn ich verifymeine private Methode nicht aufrufe (was meiner Meinung nach wahrscheinlich nicht dazugehört). Ich begrüße Ihre Kommentare dazu, ob sich Ihre Antwort ändern würde.
Duncan Jones
Es gibt viele gute Gründe, Reset zu verwenden. In diesem Fall würde ich dem Mockito-Zitat nicht allzu viel Aufmerksamkeit schenken. Wenn Sie eine Testsuite ausführen, kann Spring JUnit Class Runner zu unerwünschten Interaktionen führen, insbesondere wenn Sie Tests durchführen, die verspottete Datenbankaufrufe oder Aufrufe mit privaten Methoden beinhalten, für die Sie keine Reflektion verwenden möchten.
Sandy Simonton
Normalerweise finde ich das schwierig, wenn ich mehrere Dinge testen möchte, aber JUnit bietet einfach keine schöne (!) Möglichkeit, Tests zu parametrisieren. Im Gegensatz zu NUnit geht das beispielsweise mit Annotationen.
Stefan Hendriks
3

Absolut nicht. Wie so oft ist die Schwierigkeit, einen sauberen Test zu schreiben, eine große rote Fahne für das Design Ihres Produktionscodes. In diesem Fall ist die beste Lösung, den Code so umzugestalten, dass der Konstruktor von Bar keine Methoden aufruft.

Konstruktoren sollten konstruieren, keine Logik ausführen. Nehmen Sie den Rückgabewert der Methode und übergeben Sie ihn als Konstruktorparameter.

new Bar(mockedObject);

wird:

new Bar(mockedObject.someMethod());

Wenn dies dazu führen würde, dass diese Logik an vielen Stellen dupliziert wird, sollten Sie eine Factory-Methode erstellen, die unabhängig von Ihrem Bar-Objekt getestet werden kann:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

Wenn dieses Refactoring zu schwierig ist, ist die Verwendung von reset () eine gute Lösung. Aber lassen Sie uns klar sein - es zeigt an, dass Ihr Code schlecht gestaltet ist.

tonicsoft
quelle