Wann soll Mockito.verify () verwendet werden?

201

Ich schreibe jUnit-Testfälle für 3 Zwecke:

  1. Um sicherzustellen, dass mein Code alle erforderlichen Funktionen erfüllt, unter allen (oder den meisten) Eingabekombinationen / -werten.
  2. Um sicherzustellen, dass ich die Implementierung ändern kann, und mich auf JUnit-Testfälle zu verlassen, um mir mitzuteilen, dass alle meine Funktionen weiterhin erfüllt sind.
  3. Als Dokumentation aller Anwendungsfälle behandelt mein Code und dient als Spezifikation für das Refactoring - sollte der Code jemals neu geschrieben werden müssen. (Refaktorieren Sie den Code, und wenn meine jUnit-Tests fehlschlagen, haben Sie wahrscheinlich einen Anwendungsfall verpasst.)

Ich verstehe nicht, warum oder wann Mockito.verify()verwendet werden sollte. Wenn ich sehe, verify()dass ich angerufen werde, sagt mir das, dass meine jUnit auf die Implementierung aufmerksam wird. (Eine Änderung meiner Implementierung würde meine jUnits beschädigen, obwohl meine Funktionalität nicht beeinträchtigt wurde.)

Ich suche:

  1. Was sollten die Richtlinien für eine angemessene Verwendung sein Mockito.verify()?

  2. Ist es grundsätzlich richtig, dass sich jUnits der Implementierung der zu testenden Klasse bewusst sind oder eng damit verbunden sind?

Russell
quelle
1
Ich versuche, mich aus dem gleichen Grund, den Sie offengelegt haben, von der Verwendung von verify () so weit wie möglich fernzuhalten (ich möchte nicht, dass mein Komponententest auf die Implementierung aufmerksam wird), aber es gibt einen Fall, in dem ich keine Wahl habe - Stubbed Void-Methoden. Da sie nichts zurückgeben, tragen sie im Allgemeinen nicht zu Ihrer „tatsächlichen“ Ausgabe bei. Trotzdem müssen Sie wissen, dass es aufgerufen wurde. Ich stimme Ihnen jedoch zu, dass es keinen Sinn macht, verify zu verwenden, um den Ausführungsfluss zu überprüfen.
Legna

Antworten:

78

Wenn der Vertrag der Klasse A die Tatsache enthält, dass er Methode B eines Objekts vom Typ C aufruft, sollten Sie dies testen, indem Sie einen Mock vom Typ C erstellen und überprüfen, ob Methode B aufgerufen wurde.

Dies impliziert, dass der Vertrag der Klasse A so detailliert ist, dass er vom Typ C spricht (der eine Schnittstelle oder eine Klasse sein kann). Ja, wir sprechen von einer Spezifikationsebene, die über die "Systemanforderungen" hinausgeht und die Implementierung beschreibt.

Dies ist normal für Unit-Tests. Wenn Sie Unit-Tests durchführen, möchten Sie sicherstellen, dass jede Einheit das "Richtige" tut, und dies schließt normalerweise ihre Interaktionen mit anderen Einheiten ein. "Einheiten" können hier Klassen oder größere Teilmengen Ihrer Anwendung bedeuten.

Aktualisieren:

Ich bin der Meinung, dass dies nicht nur für die Überprüfung gilt, sondern auch für das Stubbing. Sobald Sie eine Methode einer Collaborator-Klasse stubben, ist Ihr Unit-Test in gewissem Sinne von der Implementierung abhängig geworden. Es liegt in der Natur von Unit-Tests, dies zu tun. Da es bei Mockito sowohl um Stubbing als auch um Verifizierung geht, bedeutet die Tatsache, dass Sie Mockito überhaupt verwenden, dass Sie auf diese Art von Abhängigkeit stoßen werden.

Wenn ich die Implementierung einer Klasse ändere, muss ich meiner Erfahrung nach häufig die Implementierung ihrer Komponententests entsprechend ändern. Normalerweise muss ich jedoch nicht das Inventar der Unit-Tests ändern, die es für die Klasse gibt. es sei denn natürlich, der Grund für die Änderung war das Vorhandensein einer Bedingung, die ich zuvor nicht getestet habe.

Darum geht es also bei Unit-Tests. Ein Test, der nicht unter dieser Art von Abhängigkeit von der Art und Weise leidet, wie Collaborator-Klassen verwendet werden, ist tatsächlich ein Subsystemtest oder ein Integrationstest. Natürlich werden diese häufig auch mit JUnit geschrieben und beinhalten häufig die Verwendung von Spott. Meiner Meinung nach ist "JUnit" ein schrecklicher Name für ein Produkt, mit dem wir alle Arten von Tests erstellen können.

Dawood ibn Kareem
quelle
8
Danke, David. Nach dem Durchsuchen einiger Codesätze scheint dies eine gängige Praxis zu sein. Für mich bedeutet dies jedoch, dass der Zweck der Erstellung von Komponententests nicht mehr erfüllt wird und nur der Aufwand für die Verwaltung dieser Tests für einen sehr geringen Wert erhöht wird. Ich verstehe, warum Mocks erforderlich sind und warum die Abhängigkeiten für die Ausführung des Tests eingerichtet werden müssen. Die Überprüfung, dass die Methode dependencyA.XYZ () ausgeführt wird, macht die Tests meiner Meinung nach sehr spröde.
Russell
@Russell Auch wenn "Typ C" eine Schnittstelle für einen Wrapper um eine Bibliothek oder um ein bestimmtes Subsystem Ihrer Anwendung ist?
Dawood ibn Kareem
1
Ich würde nicht sagen, dass es völlig nutzlos ist, sicherzustellen, dass ein Subsystem oder ein Dienst aufgerufen wurde - nur, dass es einige Richtlinien geben sollte (die Formulierung war das, was ich tun wollte). Zum Beispiel: (Ich übertreibe es wahrscheinlich zu sehr) Angenommen, ich verwende StrUtil.equals () in meinem Code und entscheide mich, in der Implementierung zu StrUtil.equalsIgnoreCase () zu wechseln. Wenn jUnit (StrUtil.equals) überprüft hat ), mein Test könnte fehlschlagen, obwohl die Implementierung korrekt ist. Dieser Überprüfungsaufruf, IMO, ist eine schlechte Praxis, obwohl er für Bibliotheken / Subsysteme gilt. Andererseits kann die Verwendung von verify, um sicherzustellen, dass ein Aufruf von closeDbConn ein gültiger Anwendungsfall ist.
Russell
1
Ich verstehe dich und stimme dir vollkommen zu. Ich bin aber auch der Meinung, dass das Schreiben der von Ihnen beschriebenen Richtlinien zum Schreiben eines gesamten TDD- oder BDD-Lehrbuchs führen könnte. Um Ihr Beispiel zu nennen: Anrufen equals()oder equalsIgnoreCase()niemals etwas, das in den Anforderungen einer Klasse angegeben wurde, würde also niemals einen Komponententest an sich haben. Das "Schließen der DB-Verbindung nach Abschluss" (was auch immer dies für die Implementierung bedeutet) kann jedoch eine Anforderung einer Klasse sein, auch wenn es keine "Geschäftsanforderung" ist. Für mich kommt es auf die Beziehung zwischen dem Vertrag an ...
Dawood ibn Kareem
... einer Klasse, wie in ihren Geschäftsanforderungen ausgedrückt, und der Reihe von Testmethoden, mit denen diese Klasse getestet wird. Die Definition dieser Beziehung wäre ein wichtiges Thema in jedem Buch über TDD oder BDD. Während jemand im Mockito-Team einen Beitrag zu diesem Thema für sein Wiki schreiben könnte, sehe ich nicht, wie er sich von vielen anderen verfügbaren Literaturen unterscheiden würde. Wenn Sie sehen, wie es sich unterscheiden könnte, lassen Sie es mich wissen, und vielleicht können wir gemeinsam daran arbeiten.
Dawood ibn Kareem
60

Davids Antwort ist natürlich richtig, erklärt aber nicht ganz, warum Sie das wollen würden.

Grundsätzlich testen Sie beim Testen von Einheiten eine Funktionseinheit isoliert. Sie testen, ob die Eingabe die erwartete Ausgabe erzeugt. Manchmal müssen Sie auch Nebenwirkungen testen. Kurz gesagt, überprüfen Sie, ob Sie dies tun können.

Zum Beispiel haben Sie ein bisschen Geschäftslogik, die Dinge mit einem DAO speichern soll. Sie können dies mithilfe eines Integrationstests tun, der das DAO instanziiert, es mit der Geschäftslogik verbindet und dann in der Datenbank herumstochert, um festzustellen, ob das erwartete Material gespeichert wurde. Das ist kein Unit Test mehr.

Oder Sie können das DAO verspotten und überprüfen, ob es wie erwartet aufgerufen wird. Mit mockito können Sie überprüfen, ob etwas aufgerufen wird, wie oft es aufgerufen wird, und sogar Matcher für die Parameter verwenden, um sicherzustellen, dass es auf eine bestimmte Weise aufgerufen wird.

Die Kehrseite eines solchen Unit-Tests ist in der Tat, dass Sie die Tests an die Implementierung binden, was das Refactoring etwas schwieriger macht. Auf der anderen Seite ist ein guter Designgeruch die Menge an Code, die benötigt wird, um ihn richtig auszuüben. Wenn Ihre Tests sehr lang sein müssen, stimmt wahrscheinlich etwas mit dem Design nicht. Code mit vielen Nebenwirkungen / komplexen Wechselwirkungen, die getestet werden müssen, ist wahrscheinlich keine gute Sache.

Jilles van Gurp
quelle
29

Das ist eine gute Frage! Ich denke, die Hauptursache dafür ist die folgende: Wir verwenden JUnit nicht nur für Unit-Tests. Die Frage sollte also aufgeteilt werden:

  • Sollte ich Mockito.verify () für meine Integrationstests (oder andere Tests, die höher als die Einheit sind) verwenden?
  • Sollte ich Mockito.verify () in meinem Black-Box- Unit-Test verwenden?
  • Sollte ich Mockito.verify () in meinem White-Box- Unit-Test verwenden?

Wenn wir also Tests ignorieren, die höher als die Einheit sind, kann die Frage umformuliert werden: "Die Verwendung von White-Box- Unit-Tests mit Mockito.verify () schafft ein gutes Paar zwischen Unit-Test und meiner möglichen Implementierung. Kann ich eine Gray-Box erstellen?" " Unit-Test und welche Faustregeln sollte ich dafür verwenden ".

Lassen Sie uns nun all dies Schritt für Schritt durchgehen.

* - Sollte ich Mockito.verify () in meinen Integrationstests (oder anderen Tests, die höher als die Einheit sind) verwenden? * Ich denke, die Antwort lautet eindeutig nein. Außerdem sollten Sie hierfür keine Mocks verwenden. Ihr Test sollte so nah wie möglich an der tatsächlichen Anwendung sein. Sie testen den vollständigen Anwendungsfall, nicht den isolierten Teil der Anwendung.

* Black-Box vs White-Box - unit-testing * Wenn Sie mit Black-Box - Ansatz , was Sie wirklich tun , ist, liefern Sie (alle Äquivalenzklassen) eingegeben wird , ein Zustand , und Tests , dass Sie erwartete Ausgabe erhalten. Bei diesem Ansatz ist die Verwendung von Mocks im Allgemeinen gerechtfertigt (Sie ahmen nur nach, dass sie das Richtige tun; Sie möchten sie nicht testen), aber das Aufrufen von Mockito.verify () ist überflüssig.

Wenn Sie einen White-Box- Ansatz verwenden, testen Sie das Verhalten Ihres Geräts. Bei diesem Ansatz ist der Aufruf von Mockito.verify () unerlässlich. Sie sollten sicherstellen, dass sich Ihr Gerät so verhält, wie Sie es erwarten.

Daumenregeln für Gray-Box-Tests Das Problem beim White-Box-Test besteht darin, dass eine hohe Kopplung entsteht. Eine mögliche Lösung besteht darin, Gray-Box-Tests durchzuführen, keine White-Box-Tests. Dies ist eine Kombination aus Black & White Box-Tests. Sie testen das Verhalten Ihres Geräts wirklich wie beim White-Box-Testen, aber im Allgemeinen machen Sie es nach Möglichkeit implementierungsunabhängig . Wenn es möglich ist, machen Sie einfach eine Prüfung wie im Black-Box-Fall und behaupten nur, dass die Ausgabe Ihren Erwartungen entspricht. Das Wesentliche Ihrer Frage ist also, wann es möglich ist.

Das ist wirklich schwer. Ich habe kein gutes Beispiel, aber ich kann Ihnen Beispiele geben. In dem oben erwähnten Fall mit equals () vs equalsIgnoreCase () sollten Sie Mockito.verify () nicht aufrufen, sondern nur die Ausgabe bestätigen. Wenn Sie dies nicht tun konnten, teilen Sie Ihren Code auf die kleinere Einheit auf, bis Sie es tun können. Nehmen wir andererseits an, Sie haben einen @ Service und schreiben einen @ Web-Service, der im Wesentlichen ein Wrapper für Ihren @ Service ist. Er delegiert alle Aufrufe an den @ Service (und führt eine zusätzliche Fehlerbehandlung durch). In diesem Fall ist es wichtig, dass Sie Mockito.verify () aufrufen. Sie sollten nicht alle Überprüfungen duplizieren, die Sie für @Serive durchgeführt haben. Überprüfen Sie, ob Sie @Service mit der richtigen Parameterliste aufrufen.

alexsmail
quelle
Gray-Box-Tests sind eine kleine Gefahr. Ich neige dazu, es auf Dinge wie DAOs zu beschränken. Ich habe an einigen Projekten mit extrem langsamen Builds teilgenommen, weil es eine Fülle von Gray-Box-Tests, fast keine Unit-Tests und viel zu viele Blackbox-Tests gab, um das mangelnde Vertrauen in die angeblichen Greybox-Tests auszugleichen.
Jilles van Gurp
Für mich ist dies die beste verfügbare Antwort, da sie antwortet, wann Mockito.when () in einer Vielzahl von Situationen verwendet werden muss. Gut gemacht.
Michiel Leegwater
8

Ich muss sagen, dass Sie aus Sicht eines klassischen Ansatzes absolut Recht haben:

  • Wenn Sie zuerst eine Geschäftslogik Ihrer Anwendung erstellen (oder ändern) und diese dann mit (Übernahme-) Tests ( Test-Last-Ansatz ) abdecken , ist es sehr schmerzhaft und gefährlich, Tests etwas anderes über die Funktionsweise Ihrer Software mitzuteilen als Ein- und Ausgänge prüfen.
  • Wenn Sie einen testgetriebenen Ansatz praktizieren , werden Ihre Tests als erste geschrieben, geändert und spiegeln die Anwendungsfälle der Funktionalität Ihrer Software wider . Die Implementierung hängt von Tests ab. Das bedeutet manchmal, dass Sie möchten, dass Ihre Software auf eine bestimmte Art und Weise implementiert wird, z. B. wenn Sie sich auf die Methode einer anderen Komponente verlassen oder sie sogar eine bestimmte Anzahl von Malen aufrufen. Hier bietet sich Mockito.verify () an!

Es ist wichtig zu bedenken, dass es keine universellen Werkzeuge gibt. Die Art der Software, ihre Größe, Unternehmensziele und Marktsituation, Teamfähigkeit und viele andere Faktoren beeinflussen die Entscheidung, welcher Ansatz in Ihrem speziellen Fall verwendet werden soll.

Hammelion
quelle
0

Wie einige Leute sagten

  1. Manchmal haben Sie keine direkte Ausgabe, auf die Sie sich verlassen können
  2. Manchmal müssen Sie nur bestätigen, dass Ihre getestete Methode die richtigen indirekten Ausgaben an ihre Mitarbeiter sendet (die Sie verspotten).

In Bezug auf Ihre Bedenken, Ihre Tests beim Refactoring zu brechen, wird dies bei der Verwendung von Mocks / Stubs / Spies etwas erwartet. Ich meine das per Definition und nicht in Bezug auf eine bestimmte Implementierung wie Mockito. Aber Sie könnten so denken: Wenn Sie ein Refactoring durchführen müssen, das wesentliche Änderungen an der Funktionsweise Ihrer Methode bewirken würde, ist es eine gute Idee, dies mit einem TDD-Ansatz zu tun. Dies bedeutet, dass Sie Ihren Test zuerst ändern können , um das zu definieren neues Verhalten (das den Test nicht besteht), und nehmen Sie dann die Änderungen vor und lassen Sie den Test erneut bestehen.

Emanuel Luiz Lariguet Beltrame
quelle
0

In den meisten Fällen, wenn Benutzer Mockito.verify nicht mögen, wird dies verwendet, um zu überprüfen, was das getestete Gerät tut, und das bedeutet, dass Sie Ihren Test anpassen müssen, wenn sich daran etwas ändert. Aber ich denke nicht, dass das ein Problem ist. Wenn Sie in der Lage sein möchten, die Funktionsweise einer Methode zu ändern, ohne den Test ändern zu müssen, bedeutet dies im Grunde, dass Sie Tests schreiben möchten, die nicht alles testen, was Ihre Methode tut, weil Sie nicht möchten, dass sie Ihre Änderungen testet . Und das ist die falsche Denkweise.

Was wirklich ein Problem ist, ist, wenn Sie die Funktionsweise Ihrer Methode ändern können und ein Komponententest, der die Funktionalität vollständig abdecken soll, nicht fehlschlägt. Das würde bedeuten, dass unabhängig von der Absicht Ihrer Änderung das Ergebnis Ihrer Änderung nicht durch den Test abgedeckt wird.

Aus diesem Grund verspotte ich lieber so viel wie möglich: Verspotten Sie auch Ihre Datenobjekte. Dabei können Sie nicht nur mit verify überprüfen, ob die richtigen Methoden anderer Klassen aufgerufen werden, sondern auch, ob die übergebenen Daten über die richtigen Methoden dieser Datenobjekte erfasst werden. Um dies zu vervollständigen, sollten Sie die Reihenfolge testen, in der Anrufe erfolgen. Beispiel: Wenn Sie ein Datenbankentitätsobjekt ändern und dann mithilfe eines Repositorys speichern, reicht es nicht aus, zu überprüfen, ob die Setter des Objekts mit den richtigen Daten aufgerufen werden und ob die Speichermethode des Repositorys aufgerufen wird. Wenn sie in der falschen Reihenfolge aufgerufen werden, tut Ihre Methode immer noch nicht das, was sie tun soll. Ich verwende also nicht Mockito.verify, sondern erstelle ein inOrder-Objekt mit allen Mocks und verwende stattdessen inOrder.verify. Und wenn Sie es vervollständigen möchten, sollten Sie auch Mockito anrufen. verifyNoMoreInteractions am Ende und übergeben Sie alle Mocks. Andernfalls kann jemand neue Funktionen / Verhaltensweisen hinzufügen, ohne sie zu testen. Dies würde bedeuten, dass Ihre Abdeckungsstatistik nach 100% 100% betragen kann und Sie dennoch Code stapeln, der nicht bestätigt oder verifiziert wurde.

Stefan Mondelaers
quelle