Frage

Ich habe ein grundlegendes Verständnis für Schein- und Fake-Objekte, bin mir aber nicht sicher, ob ich ein Gefühl dafür habe, wann und wo ich Spott einsetzen soll – insbesondere, wenn es auf dieses Szenario zutrifft Hier.

War es hilfreich?

Lösung

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

Wenn Sie diesen Codepfad mit der tatsächlichen Abhängigkeit testen, handelt es sich nicht um einen Unit-Test;Sie führen Integrationstests durch.Das ist zwar gut und notwendig, aber es handelt sich nicht um Unit-Tests.

Wenn Ihre Abhängigkeit fehlerhaft ist, kann Ihr Test so beeinträchtigt sein, dass er ein falsch positives Ergebnis zurückgibt.Beispielsweise können Sie der Abhängigkeit einen unerwarteten Nullwert übergeben, und die Abhängigkeit löst möglicherweise nicht den Nullwert aus, wie dies dokumentiert ist.Ihr Test tritt nicht wie vorgesehen auf eine Nullargument-Ausnahme auf und der Test ist erfolgreich.

Außerdem kann es für Sie schwierig, wenn nicht sogar unmöglich sein, das abhängige Objekt während eines Tests zuverlässig dazu zu bringen, genau das zurückzugeben, was Sie wollen.Dazu gehört auch das Auslösen erwarteter Ausnahmen innerhalb von Tests.

Ein Mock ersetzt diese Abhängigkeit.Sie legen 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 ausgelöst werden 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 Unit-Test berührt.

Andere Tipps

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

Wir wollen zum Beispiel diese Methode testen sendInvitations(MailServer mailServer) Anrufe MailServer.createMessage() genau einmal, und ruft auch an MailServer.sendMessage(m) genau einmal, und es werden keine anderen Methoden aufgerufen MailServer Schnittstelle.Dies ist der Zeitpunkt, an dem wir Scheinobjekte verwenden können.

Mit Scheinobjekten, anstatt ein echtes zu übergeben MailServerImpl, oder ein Test TestMailServer, können wir eine Scheinimplementierung von übergeben MailServer Schnittstelle.Bevor wir an einem Spott vorbeikommen MailServer, „trainieren“ wir es, damit es weiß, welche Methodenaufrufe es erwartet und welche Rückgabewerte es zurückgibt.Am Ende bestätigt das Scheinobjekt, dass alle erwarteten Methoden wie erwartet aufgerufen wurden.

Das hört sich in der Theorie gut an, es gibt aber auch einige Nachteile.

Scheinmängel

Wenn Sie über ein Schein-Framework verfügen, sind Sie versucht, Scheinobjekte zu verwenden jedes Mal Sie müssen eine Schnittstelle an die zu testende Klasse übergeben.Auf diese Weise landen Sie Testen von Interaktionen, auch wenn dies nicht erforderlich ist.Leider ist das ungewollte (versehentliche) Testen von Interaktionen schlecht, denn dann testen Sie, ob eine bestimmte Anforderung auf eine bestimmte Weise implementiert wird, und nicht, dass die Implementierung das erforderliche Ergebnis erbracht hat.

Hier ist ein Beispiel im Pseudocode.Nehmen wir an, wir haben eine erstellt MySorter Klasse und wir wollen es 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 gehen wir davon aus, dass wir keinen bestimmten Sortieralgorithmus, wie z. B. die Schnellsortierung, testen möchten;in diesem Fall wäre der letztgenannte 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 MySorter, der erste Test leistet hervorragende Arbeit, um sicherzustellen, dass wir immer noch richtig sortieren, und das ist der Sinn von Tests – sie ermöglichen uns, den Code sicher zu ändern.Andererseits letzterer Test stets bricht und es ist aktiv schädlich;es behindert das Refactoring.

Verspottungen als Stubs

Mock-Frameworks erlauben häufig auch eine weniger strenge Verwendung, bei der wir nicht genau angeben müssen, wie oft Methoden aufgerufen werden sollen und welche Parameter erwartet werden;Sie ermöglichen die Erstellung von Scheinobjekten, die als verwendet werden Stubs.

Nehmen wir an, wir haben eine Methode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) das wollen wir testen.Der PdfFormatter Objekt 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 ist uns das eigentlich egal PdfFormatter Objekt, also trainieren wir es einfach so, dass es jeden Aufruf stillschweigend annimmt und einige sinnvolle vorgefertigte Rückgabewerte für alle Methoden zurückgibt sendInvitation() ruft zufällig gerade an.Wie sind wir genau auf diese Liste von Trainingsmethoden gekommen?Wir haben einfach den Test ausgeführt und die Methoden so lange hinzugefügt, bis der Test bestanden wurde.Beachten Sie, dass wir den Stub darauf trainiert haben, auf eine Methode zu reagieren, ohne eine Ahnung zu haben, warum er sie aufrufen muss. Wir haben einfach alles hinzugefügt, worüber sich der Test beschwert hat.Wir sind zufrieden, 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 von PdfFormatter werden aufgerufen und wir haben unseren Stub nicht darauf trainiert, sie zu erwarten.Und normalerweise schlägt in solchen Situationen nicht nur ein Test fehl, sondern jeder Test, der direkt oder indirekt das verwendet sendInvitations() Methode.Wir müssen all diese Tests beheben, 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 davon nicht benötigt werden.Auch hier behindert es das Refactoring.

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

Wie kann man das beheben?Leicht:

  • Versuchen Sie, wann immer möglich, echte Klassen anstelle von Mock-Klassen zu verwenden.Benutze das Echte PdfFormatterImpl.Wenn dies nicht möglich ist, ändern Sie die realen Klassen, um dies zu ermöglichen.Wenn eine Klasse in Tests nicht verwendet werden kann, deutet dies normalerweise auf Probleme mit der Klasse hin.Das Beheben der Probleme ist eine Win-Win-Situation – Sie haben die Klasse repariert und haben einen einfacheren Test.Auf der anderen Seite ist es keine Win-win-Situation, das Problem nicht zu beheben und Mocks zu verwenden – Sie haben die echte Klasse nicht repariert und haben komplexere, weniger lesbare Tests, die weitere Umgestaltungen behindern.
  • Versuchen Sie, eine einfache Testimplementierung der Schnittstelle zu erstellen, anstatt sie in jedem Test zu verspotten, und verwenden Sie diese Testklasse in allen Ihren Tests.Erstellen TestPdfFormatter das bringt nichts.Auf diese Weise können Sie es einmal für alle Tests ändern und Ihre Tests sind nicht mit langwierigen Setups überfüllt, in denen Sie Ihre Stubs trainieren.

Alles in allem haben Scheinobjekte ihren Nutzen, aber wenn sie nicht sorgfältig verwendet werden, Sie fördern oft schlechte Praktiken, testen Implementierungsdetails, behindern Refactoring und führen zu schwer lesbaren und schwer zu wartenden Tests.

Weitere Einzelheiten zu den Mängeln von Mocks finden Sie auch unter Scheinobjekte:Mängel und Anwendungsfälle.

Faustregel:

Wenn die Funktion, die Sie testen, ein kompliziertes Objekt als Parameter benötigt und es mühsam wäre, dieses Objekt einfach zu instanziieren (wenn es beispielsweise versucht, eine TCP-Verbindung herzustellen), verwenden Sie einen Mock.

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

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

Einen tollen Podcast zum Thema gibt es hier Hier

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top