Wann soll ich mich lustig machen?

137

Ich habe ein grundlegendes Verständnis der Mock und gefälschte Objekte, aber ich bin nicht sicher , habe ich das Gefühl , wann / wo spöttisch verwenden - vor allem , da es für dieses Szenario gelten würde hier .

Esteban Araya
quelle
Ich empfehle, nur Abhängigkeiten außerhalb des Prozesses zu verspotten und nur solche, deren Interaktionen extern beobachtbar sind (SMTP-Server, Nachrichtenbus usw.). Verspotten Sie nicht die Datenbank, es ist ein Implementierungsdetail. Mehr dazu hier: Enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Antworten:

121

Ein Komponententest sollte einen einzelnen Codepfad mit einer einzelnen Methode testen. Wenn die Ausführung einer Methode außerhalb dieser Methode in ein anderes Objekt und wieder zurück übergeht, besteht eine Abhängigkeit.

Wenn Sie diesen Codepfad mit der tatsächlichen Abhängigkeit testen, sind Sie kein Komponententest. Sie testen die Integration. Das ist zwar gut und notwendig, aber kein Unit-Test.

Wenn Ihre Abhängigkeit fehlerhaft ist, kann Ihr Test so beeinflusst werden, dass ein falsches Positiv zurückgegeben wird. Beispielsweise können Sie der Abhängigkeit eine unerwartete Null übergeben, und die Abhängigkeit wird möglicherweise nicht auf Null gesetzt, wie dies dokumentiert ist. Ihr Test stößt nicht wie vorgesehen auf eine Nullargumentausnahme, und der Test besteht.

Möglicherweise fällt es Ihnen auch schwer, wenn nicht unmöglich, das abhängige Objekt zuverlässig dazu zu bringen, während eines Tests genau das zurückzugeben, was Sie möchten. Dazu gehört auch das Auslösen erwarteter Ausnahmen innerhalb von Tests.

Ein Mock ersetzt diese Abhängigkeit. Sie legen die Erwartungen für Aufrufe des abhängigen Objekts fest, legen die genauen Rückgabewerte fest, die es Ihnen geben soll, um den gewünschten Test durchzuführen, und / oder welche Ausnahmen Sie auslösen sollen, damit Sie Ihren Ausnahmebehandlungscode testen können. Auf diese Weise können Sie das betreffende Gerät einfach testen.

TL; DR: Verspotten Sie jede Abhängigkeit, die Ihr Komponententest berührt.

Drew Stephens
quelle
164
Diese Antwort ist zu radikal. Unit-Tests können und sollten mehr als eine einzige Methode anwenden, solange alles zu derselben zusammenhängenden Einheit gehört. Andernfalls wäre viel zu viel Verspotten / Fälschen erforderlich, was zu komplizierten und fragilen Tests führen würde. Nur die Abhängigkeiten, die nicht wirklich zu der zu testenden Einheit gehören, sollten durch Verspottung ersetzt werden.
Rogério
10
Diese Antwort ist auch zu optimistisch. Es wäre besser, wenn @ Jan's Mängel an Scheinobjekten berücksichtigt würden.
Jeff Axelrod
1
Ist dies nicht eher ein Argument für das Einfügen von Abhängigkeiten für Tests als für das spezifische Verspotten? Sie könnten "mock" in Ihrer Antwort so ziemlich durch "stub" ersetzen. Ich bin damit einverstanden, dass Sie die signifikanten Abhängigkeiten entweder verspotten oder stummschalten sollten. Ich habe viel scheinlastigen Code gesehen, der im Grunde dazu führt, dass Teile der verspotteten Objekte neu implementiert werden. Mocks sind sicherlich keine Silberkugel.
Draemon
2
Verspotten Sie jede Abhängigkeit, die Ihr Komponententest berührt. Das erklärt alles.
Teoman Shipahi
2
TL; DR: Verspotten Sie jede Abhängigkeit, die Ihr Komponententest berührt. - das ist nicht wirklich ein toller Ansatz, sagt mockito selbst - verspotte nicht alles. (downvoted)
p_champ
167

Scheinobjekte sind nützlich, wenn Sie Interaktionen zwischen einer zu testenden Klasse und einer bestimmten Schnittstelle testen möchten .

Zum Beispiel wollen wir testen , die Methode sendInvitations(MailServer mailServer)ruft MailServer.createMessage()genau einmal, und fordert auch MailServer.sendMessage(m)genau einmal, und keine andere Methoden werden auf der genannte MailServerSchnittstelle. In diesem Fall können wir Scheinobjekte verwenden.

Mit Scheinobjekten können wir anstelle eines Real- MailServerImploder Testversuchs TestMailServereine Scheinimplementierung der MailServerSchnittstelle bestehen. Bevor wir einen Mock übergeben MailServer, "trainieren" wir ihn, damit er weiß, welche Methodenaufrufe zu erwarten sind und welche Rückgabewerte zurückzugeben sind. Am Ende behauptet das Scheinobjekt, dass alle erwarteten Methoden wie erwartet aufgerufen wurden.

Das hört sich theoretisch gut an, hat aber auch einige Nachteile.

Scheinmängel

Wenn Sie über ein Mock-Framework verfügen, sind Sie versucht, jedes Mal ein Mock-Objekt zu verwenden, wenn Sie eine Schnittstelle an die zu testende Klasse übergeben müssen. Auf diese Weise testen Sie Interaktionen, auch wenn dies nicht erforderlich ist . Leider ist ein unerwünschtes (versehentliches) Testen von Interaktionen schlecht, da Sie dann testen, ob eine bestimmte Anforderung auf eine bestimmte Weise implementiert ist, anstatt dass die Implementierung das erforderliche Ergebnis erbracht hat.

Hier ist ein Beispiel im Pseudocode. Nehmen wir an, wir haben eine MySorterKlasse erstellt und möchten sie testen:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(In diesem Beispiel nehmen wir an, dass es sich nicht um einen bestimmten Sortieralgorithmus handelt, wie z. B. eine schnelle Sortierung, die wir testen möchten. In diesem Fall wäre der letztere Test tatsächlich gültig.)

In solch einem extremen Beispiel ist es offensichtlich, warum das letztere Beispiel falsch ist. Wenn wir die Implementierung von ändern, sorgt MySorterder erste Test hervorragend dafür, dass wir immer noch richtig sortieren. Das ist der springende Punkt bei den Tests - sie ermöglichen es uns, den Code sicher zu ändern. Andererseits bricht der letztere Test immer ab und ist aktiv schädlich; es behindert das Refactoring.

Verspottet als Stummel

Mock-Frameworks ermöglichen häufig auch eine weniger strenge Verwendung, bei der nicht genau angegeben werden muss, wie oft Methoden aufgerufen werden sollen und welche Parameter erwartet werden. Sie ermöglichen das Erstellen von Scheinobjekten, die als Stubs verwendet werden .

Nehmen wir an, wir haben eine Methode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer), die wir testen möchten. Das PdfFormatterObjekt kann zum Erstellen der Einladung verwendet werden. Hier ist der Test:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

In diesem Beispiel kümmern wir uns nicht wirklich um das PdfFormatterObjekt, also trainieren wir es einfach, um jeden Aufruf leise anzunehmen und einige sinnvolle vordefinierte Rückgabewerte für alle Methoden zurückzugeben, sendInvitation()die zu diesem Zeitpunkt aufgerufen werden. Wie sind wir auf genau diese Liste von Trainingsmethoden gekommen? Wir haben den Test einfach ausgeführt und die Methoden hinzugefügt, bis der Test bestanden wurde. Beachten Sie, dass wir den Stub so trainiert haben, dass er auf eine Methode reagiert, ohne eine Ahnung zu haben, warum sie aufgerufen werden muss. Wir haben einfach alles hinzugefügt, worüber sich der Test beschwert hat. Wir freuen uns, der Test besteht.

Aber was passiert später, wenn wir uns ändern sendInvitations()oder eine andere Klasse, die sendInvitations()verwendet, um ausgefallenere PDFs zu erstellen? Unser Test schlägt plötzlich fehl, weil jetzt mehr Methoden PdfFormatteraufgerufen werden und wir unseren Stub nicht darauf trainiert haben, sie zu erwarten. Und normalerweise ist es nicht nur ein Test, der in solchen Situationen fehlschlägt, sondern jeder Test, der die sendInvitations()Methode direkt oder indirekt verwendet. Wir müssen all diese Tests korrigieren, indem wir weitere Schulungen hinzufügen. Beachten Sie auch, dass wir nicht mehr benötigte Methoden nicht entfernen können, da wir nicht wissen, welche nicht benötigt werden. Auch hier behindert es das Refactoring.

Auch die Lesbarkeit des Tests hat furchtbar gelitten. Es gibt dort viel Code, den wir nicht geschrieben haben, weil wir wollten, sondern weil wir mussten. Wir wollen diesen Code nicht dort haben. Tests, die Scheinobjekte verwenden, sehen sehr komplex aus und sind oft schwer zu lesen. Die Tests sollen dem Leser helfen, zu verstehen, wie die Klasse unter dem Test verwendet werden sollte, daher sollten sie einfach und unkompliziert sein. Wenn sie nicht lesbar sind, wird sie niemand pflegen. Tatsächlich ist es einfacher, sie zu löschen, als sie zu pflegen.

Wie kann man das beheben? Leicht:

  • Versuchen Sie, wann immer möglich, echte Klassen anstelle von Mocks zu verwenden. Verwenden Sie das echte PdfFormatterImpl. Wenn dies nicht möglich ist, ändern Sie die realen Klassen, um dies zu ermöglichen. Die Nichtverwendung einer Klasse in Tests weist normalerweise auf einige Probleme mit der Klasse hin. Das Beheben der Probleme ist eine Win-Win-Situation - Sie haben die Klasse behoben und haben einen einfacheren Test. Auf der anderen Seite ist es kein Gewinn, es nicht zu reparieren und Mocks zu verwenden - Sie haben die reale Klasse nicht repariert und Sie haben komplexere, weniger lesbare Tests, die weitere Refactorings behindern.
  • Versuchen Sie, eine einfache Testimplementierung der Schnittstelle zu erstellen, anstatt sie in jedem Test zu verspotten, und verwenden Sie diese Testklasse in all Ihren Tests. Erstellen TestPdfFormatter, das nichts tut. Auf diese Weise können Sie es für alle Tests einmal ändern, und Ihre Tests sind nicht mit langwierigen Setups überfüllt, in denen Sie Ihre Stubs trainieren.

Alles in allem haben Scheinobjekte ihre Verwendung, aber wenn sie nicht sorgfältig verwendet werden, fördern sie häufig schlechte Praktiken, testen Implementierungsdetails, behindern das Refactoring und führen zu schwer lesbaren und schwer zu wartenden Tests .

Weitere Informationen zu Mock- Mängeln finden Sie auch unter Mock-Objekte: Mängel und Anwendungsfälle .

Jan Soltis
quelle
1
Eine gut durchdachte Antwort, und ich stimme größtenteils zu. Ich würde sagen, da Unit-Tests White-Box-Tests sind, ist es möglicherweise keine unangemessene Belastung, die Tests ändern zu müssen, wenn Sie die Implementierung ändern, um schickere PDFs zu senden. Manchmal können Mocks ein nützlicher Weg sein, um Stubs schnell zu implementieren, anstatt viel Kesselplatte zu haben. In der Praxis scheint ihre Verwendung jedoch nicht auf diese einfachen Fälle beschränkt zu sein.
Draemon
1
Ist es nicht der springende Punkt bei einem Mock, dass Ihre Tests konsistent sind, dass Sie sich nicht darum kümmern müssen, Objekte zu verspotten, deren Implementierungen sich möglicherweise jedes Mal, wenn Sie Ihren Test ausführen, konsistente Testergebnisse ändern und konsistente Testergebnisse erhalten?
PositiveGuy
1
Sehr gute und relevante Punkte (insbesondere in Bezug auf Testzerbrechlichkeit). Als ich jünger war, habe ich oft Mocks verwendet, aber jetzt betrachte ich Unit-Tests, die stark von Mocks abhängen, als potenziell verfügbar und konzentriere mich mehr auf Integrationstests (mit tatsächlichen Komponenten)
Kemoda
6
"Wenn eine Klasse nicht in Tests verwendet werden kann, weist dies normalerweise auf einige Probleme mit der Klasse hin." Wenn es sich bei der Klasse um einen Dienst handelt (z. B. Zugriff auf die Datenbank oder Proxy für den Webdienst), sollte dies als externe Abhängigkeit betrachtet und verspottet / gestoppt werden
Michael Freidgeim,
1
Aber was passiert später, wenn wir sendInvitations () ändern? Wenn der zu testende Code geändert wird, garantiert er den vorherigen Vertrag nicht mehr und muss daher fehlschlagen. Und normalerweise ist es nicht nur ein Test, der in solchen Situationen fehlschlägt . In diesem Fall ist der Code nicht sauber implementiert. Die Überprüfung von Methodenaufrufen der Abhängigkeit sollte nur einmal getestet werden (im entsprechenden Unit-Test). Alle anderen Klassen verwenden nur die Scheininstanz. Ich sehe also keine Vorteile darin, Integration mit Unit-Tests zu mischen.
Christopher Will
55

Faustregel:

Wenn die zu testende Funktion ein kompliziertes Objekt als Parameter benötigt und es schwierig wäre, dieses Objekt einfach zu instanziieren (wenn beispielsweise versucht wird, eine TCP-Verbindung herzustellen), verwenden Sie ein Modell.

Orion Edwards
quelle
4

Sie sollten ein Objekt verspotten, wenn Sie eine Abhängigkeit in einer Codeeinheit haben, die Sie testen möchten und die "nur so" sein muss.

Wenn Sie beispielsweise versuchen, eine Logik in Ihrer Codeeinheit zu testen, aber etwas von einem anderen Objekt abrufen müssen und was von dieser Abhängigkeit zurückgegeben wird, kann sich dies auf das auswirken, was Sie testen möchten - verspotten Sie dieses Objekt.

Einen großartigen Podcast zum Thema finden Sie hier

Toran Billups
quelle
Der Link führt jetzt zur aktuellen Episode, nicht zur beabsichtigten Episode. Ist der beabsichtigte Podcast dieser hanselminutes.com/32/mock-objects ?
C Perkins