Gibt es einen Grund für Unit-Tests, die alles Publikum stutzen und verspotten?

58

Wenn ich Unit-Tests auf die "richtige" Art und Weise durchführe, dh jeden öffentlichen Anruf stoppe und voreingestellte Werte oder Mocks zurückgebe, habe ich das Gefühl, dass ich eigentlich nichts teste. Ich sehe mir meinen Code buchstäblich an und erstelle Beispiele, die auf dem Fluss der Logik durch meine öffentlichen Methoden basieren. Und jedes Mal, wenn sich die Implementierung ändert, muss ich diese Tests erneut ändern, ohne wirklich das Gefühl zu haben, dass ich etwas Nützliches erreiche (sei es mittel- oder langfristig). Ich mache auch Integrationstests (einschließlich Non-Happy-Paths) und die erhöhten Testzeiten stören mich nicht wirklich. Bei denen habe ich das Gefühl, dass ich tatsächlich auf Regressionen teste, weil sie mehrere abgefangen haben, während diese Unit-Tests mir nur zeigen, dass sich die Implementierung meiner öffentlichen Methode geändert hat, die ich bereits kenne.

Unit Testing ist ein riesiges Thema, und ich habe das Gefühl, dass ich hier etwas nicht verstehe. Was ist der entscheidende Vorteil von Unit-Tests gegenüber Integrationstests (ohne Zeitaufwand)?

thront
quelle
4
Meine zwei Cent: Überbeanspruchen Sie keine Mocks ( googletesting.blogspot.com/2013/05/…
Juan Mendes
1
Siehe auch "Mocking is a Code Smell"
user949300

Antworten:

37

Wenn ich Unit-Tests auf die "richtige" Art und Weise durchführe, dh jeden öffentlichen Anruf stoppe und voreingestellte Werte oder Mocks zurückgebe, habe ich das Gefühl, dass ich eigentlich nichts teste. Ich sehe mir meinen Code buchstäblich an und erstelle Beispiele, die auf dem Fluss der Logik durch meine öffentlichen Methoden basieren.

Dies hört sich so an, als ob die zu testende Methode mehrere andere Klasseninstanzen benötigt (die Sie verspotten müssen) und mehrere Methoden selbst aufruft.

Diese Art von Code ist aus den von Ihnen genannten Gründen in der Tat schwer zu testen.

Was ich als hilfreich empfunden habe, ist die Aufteilung solcher Klassen in:

  1. Klassen mit der eigentlichen "Geschäftslogik". Diese verwenden nur wenige oder gar keine Aufrufe an andere Klassen und sind einfach zu testen (Wert (e) ein - Wert aus).
  2. Klassen, die mit externen Systemen (Dateien, Datenbanken usw.) verbunden sind. Diese umhüllen das externe System und bieten eine komfortable Schnittstelle für Ihre Anforderungen.
  3. Klassen, die "alles zusammenhalten"

Dann sind die Klassen von 1. einfach zu testen, da sie nur Werte akzeptieren und ein Ergebnis zurückgeben. In komplexeren Fällen müssen diese Klassen möglicherweise selbst Aufrufe ausführen, sie rufen jedoch nur Klassen ab 2. auf (und rufen nicht direkt z. B. eine Datenbankfunktion auf), und die Klassen ab 2. sind einfach zu verspotten (weil nur sie) Legen Sie die Teile des verpackten Systems frei, die Sie benötigen.

Die Klassen von 2. und 3. können normalerweise nicht sinnvoll auf ihre Einheit getestet werden (da sie für sich genommen nichts Nützliches tun, sondern nur "Kleber" -Code sind). OTOH, diese Klassen sind in der Regel relativ einfach (und nur wenige), daher sollten sie durch Integrationstests angemessen abgedeckt werden.


Ein Beispiel

Eine Klasse

Angenommen, Sie haben eine Klasse, die einen Preis aus einer Datenbank abruft, einige Rabatte anwendet und dann die Datenbank aktualisiert.

Wenn Sie dies alles in einer Klasse haben, müssen Sie DB-Funktionen aufrufen, die schwer zu verspotten sind. Im Pseudocode:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Alle drei Schritte benötigen DB-Zugriff, daher viel (komplexes) Mocking, das wahrscheinlich kaputt geht, wenn sich der Code oder die DB-Struktur ändern.

Aufteilen

Sie haben in drei Klassen aufgeteilt: PriceCalculation, PriceRepository, App.

PriceCalculation führt nur die eigentliche Berechnung durch und erhält die benötigten Werte. App verbindet alles miteinander:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

Dieser Weg:

  • PriceCalculation kapselt die "Geschäftslogik". Es ist einfach zu testen, da es nichts von sich aus aufruft.
  • PriceRepository kann auf Pseudoeinheiten getestet werden, indem eine Scheindatenbank eingerichtet und die Lese- und Aktualisierungsaufrufe getestet werden. Es hat wenig Logik, daher wenige Codepfade, sodass Sie nicht zu viele dieser Tests benötigen.
  • App kann nicht sinnvoll Unit-getestet werden, weil es Kleber-Code ist. Es ist jedoch auch sehr einfach, daher sollten Integrationstests ausreichen. Wenn die spätere App zu komplex wird, werden mehr "Geschäftslogik" -Klassen aufgeteilt.

Schließlich kann sich herausstellen, dass PriceCalculation seine eigenen Datenbankaufrufe durchführen muss. Zum Beispiel, weil nur PriceCalculation weiß, welche Daten benötigt werden, sodass sie von App nicht im Voraus abgerufen werden können. Anschließend können Sie eine auf die Anforderungen von PriceCalculation zugeschnittene Instanz von PriceRepository (oder einer anderen Repository-Klasse) übergeben. Diese Klasse muss dann verspottet werden, dies ist jedoch einfach, da die Schnittstelle von PriceRepository einfach ist, z PriceRepository.getPrice(articleNo, contractType). Am wichtigsten ist, dass die PriceRepository-Schnittstelle PriceCalculation von der Datenbank isoliert, sodass Änderungen am DB-Schema oder an der Datenorganisation wahrscheinlich nicht zu einer Änderung der Schnittstelle und damit zu einer Unterbrechung der Mocks führen.

sleske
quelle
5
Ich dachte, ich wäre alleine und würde nicht alles testen, danke
enthrops
4
Ich bin einfach anderer Meinung, wenn Sie sagen, dass es nur wenige Klassen vom Typ 3 gibt. Ich glaube, der größte Teil meines Codes ist vom Typ 3 und es gibt fast keine Geschäftslogik. Das meine ich: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz
27

Was ist der entscheidende Vorteil von Unit-Tests gegenüber Integrationstests?

Das ist eine falsche Zweiteilung.

Unit-Tests und Integrationstests dienen zwei ähnlichen, aber unterschiedlichen Zwecken. Der Zweck von Unit-Tests ist es, sicherzustellen, dass Ihre Methoden funktionieren. In der Praxis stellen die Komponententests sicher, dass der Code den in den Komponententests festgelegten Vertrag erfüllt. Dies zeigt sich in der Art und Weise, in der Komponententests entworfen wurden: Sie geben speziell an, was der Code tun soll, und versichern, dass der Code dies tut.

Integrationstests sind unterschiedlich. Integrationstests üben die Interaktion zwischen Softwarekomponenten aus. Es können Softwarekomponenten vorhanden sein, die alle ihre Tests bestehen und trotzdem Integrationstests nicht bestehen, weil sie nicht ordnungsgemäß interagieren.

Wenn Unit-Tests jedoch einen entscheidenden Vorteil haben, ist dies der Fall : Unit-Tests sind viel einfacher einzurichten und erfordern viel weniger Zeit und Aufwand als Integrationstests. Bei richtiger Anwendung fördern Komponententests die Entwicklung von "testbarem" Code, was bedeutet, dass das Endergebnis zuverlässiger, verständlicher und leichter zu warten ist. Testbarer Code weist bestimmte Merkmale auf, z. B. eine kohärente API und wiederholbares Verhalten, und gibt Ergebnisse zurück, die leicht zu bestätigen sind.

Integrationstests sind schwieriger und kostenintensiver, da Sie häufig aufwändige Verspottungen, komplexe Einstellungen und schwierige Behauptungen benötigen. Stellen Sie sich vor, Sie versuchen auf der höchsten Ebene der Systemintegration die menschliche Interaktion in einer Benutzeroberfläche zu simulieren. Ganze Softwaresysteme widmen sich dieser Art von Automatisierung. Und es ist die Automatisierung, die wir suchen. Tests am Menschen sind nicht wiederholbar und lassen sich nicht wie automatisierte Tests skalieren.

Schließlich gibt der Integrationstest keine Garantie für die Codeabdeckung. Wie viele Kombinationen von Codeschleifen, Bedingungen und Zweigen testen Sie mit Ihren Integrationstests? Weißt du es wirklich? Es gibt Tools, die Sie für Komponententests und getestete Methoden verwenden können und die Ihnen Aufschluss über die Codeabdeckung und die zyklomatische Komplexität Ihres Codes geben. Wirklich gut funktionieren sie aber nur auf Methodenebene, wo Unit-Tests leben.


Wenn sich Ihre Tests bei jedem Refactor ändern, ist das ein anderes Problem. Unit - Tests sollen über sein dokumentieren , was eine Software macht, was beweist , dass es das tut, und dann beweisen , dass es das tut wieder , wenn Sie die zugrunde liegende Implementierung Refactoring. Wenn sich Ihre API ändert oder Sie Ihre Methoden aufgrund einer Änderung im Systemdesign ändern müssen, ist dies das, was passieren soll. Wenn es häufig vorkommt, sollten Sie zuerst Ihre Tests schreiben, bevor Sie Code schreiben. Dies zwingt Sie, über die Gesamtarchitektur nachzudenken und Code mit der bereits eingerichteten API zu schreiben.

Wenn Sie viel Zeit damit verbringen, Komponententests für Trivialcode zu schreiben, wie z

public string SomeProperty { get; set; }

dann sollten Sie Ihren Ansatz überprüfen. Unit-Tests sollen das Verhalten testen , und es gibt kein Verhalten in der obigen Codezeile. Sie haben jedoch irgendwo eine Abhängigkeit in Ihrem Code erstellt, da diese Eigenschaft mit ziemlicher Sicherheit an anderer Stelle in Ihrem Code referenziert wird. Schreiben Sie stattdessen Methoden, die die erforderliche Eigenschaft als Parameter akzeptieren:

public string SomeMethod(string someProperty);

Jetzt hat Ihre Methode keine Abhängigkeiten von etwas außerhalb von sich selbst und ist jetzt testbarer, da sie vollständig in sich geschlossen ist. Zugegeben, Sie werden dies nicht immer tun können, aber es verschiebt Ihren Code in die Richtung, testbarer zu sein, und dieses Mal schreiben Sie einen Unit-Test für das tatsächliche Verhalten.

Robert Harvey
quelle
2
Mir ist bewusst, dass Unit-Tests und Integrationstests Server unterschiedliche Ziele verfolgen. Ich verstehe jedoch immer noch nicht, wie nützlich Unit-Tests sind, wenn Sie alle öffentlichen Aufrufe von Unit-Tests stummschalten und verspotten. Ich würde verstehen, dass "Code den in den Unit-Tests festgelegten Vertrag erfüllt", wenn es keine Stubs und Mocks gäbe. Meine Unit-Tests sind buchstäblich Reflexionen der Logik innerhalb der Methoden, die ich teste. Sie (ich) testen eigentlich nichts, sehen sich nur Ihren Code an und wandeln ihn in Tests um. In Bezug auf Schwierigkeiten bei der Automatisierung und Codeabdeckung mache ich derzeit Rails, und beide sind gut aufgehoben.
Enthrops
2
Wenn Ihre Tests nur Reflexionen der Methodenlogik sind, machen Sie es falsch. Ihre Unit-Tests sollten der Methode im Wesentlichen einen Wert übergeben, einen Rückgabewert akzeptieren und eine Aussage darüber machen, wie dieser Rückgabewert lauten sollte. Dazu ist keine Logik erforderlich.
Robert Harvey
2
Ergibt Sinn, muss aber noch alle anderen öffentlichen Aufrufe (db, einige 'Globals', wie den aktuellen Benutzerstatus usw.) stoppen und am Ende den Code anhand der Methodenlogik testen.
Enthrops
1
Ich schätze, Unit-Tests sind für meist isolierte Dinge gedacht, die sich als 'Eingaben -> erwartete Ergebnisse' bestätigen lassen.
Enthrops
1
Meine Erfahrung in der Erstellung zahlreicher Unit - und Integrationstests (ganz zu schweigen von den von diesen Tests verwendeten erweiterten Tools für Verspottung, Integrationstests und Codeabdeckung) widerspricht den meisten Ihrer Behauptungen hier: 1) "Der Zweck von Unit - Tests besteht darin, sicherzustellen, dass Ihre Code macht was er will ": Gleiches gilt für Integrationstests (umso mehr); 2) "Unit-Tests sind viel einfacher einzurichten": Nein, das sind sie nicht (ziemlich oft sind es die Integrationstests, die einfacher sind); 3) "Bei sachgemäßer Verwendung fördern Komponententests die Entwicklung von" testbarem "Code": Gleiches gilt für Integrationstests. (Fortsetzung)
Rogério
4

Die Unit-Tests mit Mocks sollen sicherstellen, dass die Implementierung der Klasse korrekt ist. Sie verspotten die öffentlichen Schnittstellen der Abhängigkeiten des Codes, den Sie testen. Auf diese Weise haben Sie die Kontrolle über alles, was außerhalb der Klasse liegt, und können sicher sein, dass ein fehlgeschlagener Test auf etwas innerhalb der Klasse und nicht auf eines der anderen Objekte zurückzuführen ist.

Sie testen auch das Verhalten der getesteten Klasse und nicht die Implementierung. Wenn Sie den Code überarbeiten (neue interne Methoden usw. erstellen), sollten die Komponententests nicht fehlschlagen. Wenn Sie jedoch die öffentliche Methode ändern, sollten die Tests auf jeden Fall fehlschlagen, da Sie das Verhalten geändert haben.

Es klingt auch so, als würden Sie die Tests schreiben, nachdem Sie den Code geschrieben haben. Versuchen Sie stattdessen, die Tests zuerst zu schreiben. Versuchen Sie, das Verhalten der Klasse zu skizzieren, und schreiben Sie dann die Mindestmenge an Code, damit die Tests bestanden werden.

Sowohl Unit-Tests als auch Integrationstests sind nützlich, um die Qualität Ihres Codes sicherzustellen. Die Komponententests untersuchen jede Komponente für sich. Und die Integrationstests stellen sicher, dass alle Komponenten richtig interagieren. Ich möchte beide Typen in meiner Testsuite haben.

Unit-Tests haben mir bei meiner Entwicklung geholfen, da ich mich jeweils auf einen Teil der Anwendung konzentrieren kann. Verspotten der Komponenten, die ich noch nicht gemacht habe. Sie eignen sich auch hervorragend zur Regression, da sie alle Fehler in der von mir gefundenen Logik dokumentieren (sogar in den Unit-Tests).

AKTUALISIEREN

Das Erstellen eines Tests, der nur sicherstellt, dass Methoden aufgerufen werden, hat den Wert, dass sichergestellt wird, dass die Methoden tatsächlich aufgerufen werden. Insbesondere wenn Sie Ihre Tests zuerst schreiben, haben Sie eine Checkliste der Methoden, die durchgeführt werden müssen. Da dieser Code so ziemlich prozedural ist, müssen Sie nicht viel überprüfen, außer dass die Methoden aufgerufen werden. Sie schützen den Code für zukünftige Änderungen. Wenn Sie eine Methode vor der anderen aufrufen müssen. Oder dass eine Methode immer aufgerufen wird, auch wenn die ursprüngliche Methode eine Ausnahme auslöst.

Der Test für diese Methode wird möglicherweise nie oder nur dann geändert, wenn Sie die Methoden ändern. Warum ist das eine schlechte Sache? Es hilft, die Tests zu verstärken. Wenn Sie einen Test nach dem Ändern des Codes reparieren müssen, werden Sie die Gewohnheit haben, die Tests mit dem Code zu ändern.

Schleis
quelle
Und wenn eine Methode nichts privates nennt, dann macht es keinen Sinn, sie in einem Unit-Test zu testen, richtig?
Enthrops
Wenn die Methode privat ist, testen Sie sie nicht explizit, sondern sollten sie über die öffentliche Schnittstelle testen. Alle öffentlichen Methoden sollten getestet werden, um sicherzustellen, dass das Verhalten korrekt ist.
Schleis
Nein, ich meine, wenn eine öffentliche Methode nichts Privates aufruft, ist es sinnvoll, diese öffentliche Methode zu testen?
Enthrops
Ja. Die Methode macht etwas, nicht wahr? Also sollte es getestet werden. Vom Standpunkt des Testens aus weiß ich nicht, ob es sich um etwas Privates handelt. Ich weiß nur, dass ich, wenn ich Input A zur Verfügung stelle, Output B bekommen sollte.
Schleis
Oh ja, die Methode macht etwas und dass etwas andere öffentliche Methoden aufruft (und nur das). Die Art und Weise, wie Sie "richtig" testen würden, besteht darin, die Aufrufe mit einigen Rückgabewerten zu stoppen und dann die Nachrichtenerwartungen einzurichten. Was genau testen Sie in diesem Fall? Dass die richtigen Anrufe getätigt werden? Nun, Sie haben diese Methode geschrieben und können sie sich ansehen und genau sehen, was sie bewirkt. Ich denke, Unit-Tests eignen sich eher für isolierte Methoden, die wie "Eingabe -> Ausgabe" verwendet werden sollen. Sie können also eine Reihe von Beispielen erstellen und dann beim Refactor Regressionstests durchführen.
Enthrops
3

Ich hatte eine ähnliche Frage - bis ich die Leistungsfähigkeit von Komponententests entdeckte. Kurz gesagt, sie sind die gleichen wie Unit-Tests, mit der Ausnahme, dass Sie nicht standardmäßig verspotten, sondern echte Objekte verwenden (idealerweise über die Abhängigkeitsinjektion).

Auf diese Weise können Sie schnell zuverlässige Tests mit einer guten Codeabdeckung erstellen. Sie müssen Ihre Mocks nicht ständig aktualisieren. Es mag ein bisschen ungenauer sein als Unit-Tests mit 100% igen Verspottungen, aber die Zeit und das Geld, die Sie sparen, kompensieren das. Das einzige, wofür Sie wirklich Mocks oder Fixtures verwenden müssen, sind Speicher-Backends oder externe Services.

Tatsächlich ist exzessives Verspotten ein Anti-Pattern: TDD Anti-Patterns und Mocks sind böse .

lastzero
quelle
0

Obwohl die Operation bereits eine Antwort markiert hat, füge ich hier nur meine 2 Cent hinzu.

Was ist der entscheidende Vorteil von Unit-Tests gegenüber Integrationstests (ohne Zeitaufwand)?

Und auch als Antwort auf

Wenn ich Unit-Tests auf die "richtige" Art und Weise durchführe, dh jeden öffentlichen Anruf stoppe und voreingestellte Werte oder Mocks zurückgebe, habe ich das Gefühl, dass ich eigentlich nichts teste.

Es gibt ein hilfreiches, aber nicht genau das, wonach OP gefragt hat:

Unit Tests funktionieren, aber es gibt immer noch Fehler?

Aufgrund meiner geringen Erfahrung mit Testsuiten verstehe ich, dass Unit-Tests immer die grundlegendsten Funktionen einer Klasse auf Methodenebene testen. Meiner Meinung nach verdient jede Methode, ob öffentlich, privat oder intern , einen eigenen Komponententest. Sogar in meiner jüngsten Erfahrung hatte ich eine öffentliche Methode, die eine andere kleine private Methode aufrief. Es gab also zwei Ansätze:

  1. Erstellen Sie keine Komponententests für private Methoden.
  2. Erstellen Sie einen oder mehrere Komponententests für eine private Methode.

Wenn Sie logisch denken, ist der Sinn einer privaten Methode: Die öffentliche Hauptmethode wird zu umfangreich oder unübersichtlich. Um dies zu lösen, überarbeiten Sie die Struktur mit Bedacht und erstellen kleine Codestücke, die es verdienen, separate private Methoden zu sein, wodurch Ihre öffentliche Hauptmethode weniger umfangreich wird. Sie überarbeiten dies, indem Sie berücksichtigen, dass diese private Methode später möglicherweise wiederverwendet wird. Es kann Fälle geben, in denen es keine andere öffentliche Methode gibt, die von dieser privaten Methode abhängt, aber die Zukunft kennt.


Betrachtet man den Fall, in dem eine private Methode von vielen anderen öffentlichen Methoden wiederverwendet wird.

Also, wenn ich Ansatz 1 gewählt hätte: Ich hätte Unit-Tests dupliziert und sie wären kompliziert gewesen, da Sie für jeden Zweig der öffentlichen Methode sowie der privaten Methode eine Anzahl von Unit-Tests hatten.

Wenn ich Ansatz 2 gewählt hätte: Der für Unit-Tests geschriebene Code wäre relativ kürzer und viel einfacher zu testen.


Betrachtet man den Fall, dass die private Methode nicht wiederverwendet wird, ist es nicht sinnvoll, einen separaten Komponententest für diese Methode zu schreiben.

Was als Integrationstests betroffen sind, neigen sie erschöpfend und von hohem Niveau zu sein. Sie werden Ihnen mitteilen, dass alle Ihre Klassen zu diesem endgültigen Ergebnis kommen sollten. Weitere Informationen zur Nützlichkeit von Integrationstests finden Sie unter dem angegebenen Link.

Shankbond
quelle