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 .
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 .
Antworten:
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.
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.
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.
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:
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.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 .
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.
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