Ich schreibe eine Komponente, die angesichts einer ZIP-Datei Folgendes benötigt:
- Entpacken Sie die Datei.
- Suchen Sie eine bestimmte DLL unter den entpackten Dateien.
- Laden Sie diese DLL durch Reflektion und rufen Sie eine Methode darauf auf.
Ich möchte diese Komponente einem Unit-Test unterziehen.
Ich bin versucht, Code zu schreiben, der sich direkt mit dem Dateisystem befasst:
void DoIt()
{
Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
myDll.InvokeSomeSpecialMethod();
}
Aber die Leute sagen oft: "Schreiben Sie keine Komponententests, die auf dem Dateisystem, der Datenbank, dem Netzwerk usw. beruhen."
Wenn ich dies auf Unit-Test-freundliche Weise schreiben würde, würde es wahrscheinlich so aussehen:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
Yay! Jetzt ist es testbar; Ich kann der DoIt-Methode Test-Doubles (Mocks) hinzufügen. Aber zu welchen Kosten? Ich musste jetzt 3 neue Schnittstellen definieren, um dies testbar zu machen. Und was genau teste ich? Ich teste, ob meine DoIt-Funktion ordnungsgemäß mit ihren Abhängigkeiten interagiert. Es wird nicht getestet, ob die Zip-Datei ordnungsgemäß entpackt wurde usw.
Es fühlt sich nicht mehr so an, als würde ich die Funktionalität testen. Es fühlt sich an, als würde ich nur Klasseninteraktionen testen.
Meine Frage lautet : Was ist der richtige Weg, um etwas zu testen, das vom Dateisystem abhängig ist?
Bearbeiten Ich bin mit .NET, aber das Konzept könnte Java oder nativen Code beantragen.
quelle
myDll.InvokeSomeSpecialMethod();
dem Sie überprüfen würden, ob er sowohl in Erfolgs- als auch in Misserfolgssituationen korrekt funktioniert, sodass ich keinen Unit-Test durchführen würde. Dies bedeutetDoIt
jedochDllRunner.Run
, dass ein UNIT-Test missbraucht wird, um zu überprüfen, ob der gesamte Prozess funktioniert ein akzeptabler Missbrauch und da es sich um einen Integrationstest handelt, der einen Unit-Test maskiert, müssen die normalen Unit-Test-Regeln nicht so streng angewendet werdenAntworten:
Daran ist wirklich nichts auszusetzen, es ist nur eine Frage, ob Sie es einen Unit-Test oder einen Integrationstest nennen. Sie müssen nur sicherstellen, dass bei der Interaktion mit dem Dateisystem keine unbeabsichtigten Nebenwirkungen auftreten. Stellen Sie insbesondere sicher, dass Sie nach sich selbst bereinigen - alle von Ihnen erstellten temporären Dateien löschen - und dass Sie nicht versehentlich eine vorhandene Datei überschreiben, die zufällig denselben Dateinamen wie eine von Ihnen verwendete temporäre Datei hat. Verwenden Sie immer relative Pfade und keine absoluten Pfade.
Es wäre auch eine gute Idee,
chdir()
vor dem Ausführen des Tests in ein temporäres Verzeichnis zu wechseln undchdir()
danach wieder zurückzukehren.quelle
chdir()
ist, sodass Sie möglicherweise die Möglichkeit beeinträchtigen, Ihre Tests parallel auszuführen, wenn Ihr Testframework oder eine zukünftige Version davon dies unterstützt.Sie haben den Nagel direkt auf den Kopf getroffen. Was Sie testen möchten, ist die Logik Ihrer Methode, nicht unbedingt, ob eine echte Datei adressiert werden kann. Sie müssen (in diesem Komponententest) nicht testen, ob eine Datei korrekt entpackt wurde. Ihre Methode nimmt dies als selbstverständlich an. Die Schnittstellen sind für sich genommen wertvoll, da sie Abstraktionen bereitstellen, gegen die Sie programmieren können, anstatt sich implizit oder explizit auf eine konkrete Implementierung zu verlassen.
quelle
DoIt
Funktion muss nicht einmal getestet werden. Wie der Fragesteller zu Recht betonte, gibt es nichts mehr zu testen. Jetzt ist es die Implementierung vonIZipper
,IFileSystem
undIDllRunner
das muss getestet werden, aber genau diese Dinge wurden für den Test verspottet!Ihre Frage enthüllt einen der schwierigsten Teile des Testens für Entwickler, die gerade erst damit anfangen:
"Was zum Teufel teste ich?"
Ihr Beispiel ist nicht sehr interessant, da es nur einige API-Aufrufe zusammenklebt. Wenn Sie also einen Komponententest dafür schreiben würden, würden Sie am Ende nur behaupten, dass Methoden aufgerufen wurden. Tests wie dieser koppeln Ihre Implementierungsdetails eng mit dem Test. Dies ist schlecht, da Sie den Test jetzt jedes Mal ändern müssen, wenn Sie die Implementierungsdetails Ihrer Methode ändern, da das Ändern der Implementierungsdetails Ihre Tests unterbricht!
Schlechte Tests zu haben ist tatsächlich schlimmer als überhaupt keine Tests zu haben.
In Ihrem Beispiel:
Während Sie Mocks übergeben können, gibt es keine Logik in der zu testenden Methode. Wenn Sie einen Unit-Test dafür versuchen würden, könnte dies ungefähr so aussehen:
Herzlichen Glückwunsch, Sie haben die Implementierungsdetails Ihrer
DoIt()
Methode im Grunde genommen in einen Test kopiert . Viel Spaß beim Unterhalten.Wenn Sie Tests schreiben, möchten Sie das WAS und nicht das WIE testen . Weitere Informationen finden Sie unter Black-Box-Tests .
Das WAS ist der Name Ihrer Methode (oder sollte es zumindest sein). Das WIE sind all die kleinen Implementierungsdetails, die in Ihrer Methode enthalten sind. Gute Tests ermöglichen es Ihnen, das WIE auszutauschen, ohne das WAS zu brechen .
Denken Sie so darüber nach, fragen Sie sich:
"Wenn ich die Implementierungsdetails dieser Methode ändere (ohne den öffentlichen Auftrag zu ändern), werden meine Tests dadurch unterbrochen?"
Wenn die Antwort ja lautet, testen Sie das WIE und nicht das WAS .
Um Ihre spezielle Frage zum Testen von Code mit Dateisystemabhängigkeiten zu beantworten, nehmen wir an, Sie hatten etwas Interessanteres mit einer Datei vor und wollten den Base64-codierten Inhalt von a
byte[]
in einer Datei speichern. Sie können Streams verwenden, um zu testen, ob Ihr Code das Richtige tut, ohne überprüfen zu müssen, wie er es tut. Ein Beispiel könnte so etwas sein (in Java):Der Test verwendet eine
ByteArrayOutputStream
aber in der Anwendung (mit Dependency Injection) dem eigentlichen StreamFactory (vielleicht FileStreamFactory genannt) zurückkehren würdeFileOutputStream
vonoutputStream()
und zu einem schreiben würdeFile
.Das Interessante an der
write
Methode hier war, dass sie den Inhalt aus Base64-codiert herausschrieb, also haben wir darauf getestet. Für IhreDoIt()
Methode würde dies mit einem Integrationstest angemessener getestet .quelle
Ich bin zurückhaltend, meinen Code mit Typen und Konzepten zu verschmutzen, die nur existieren, um das Testen von Einheiten zu erleichtern. Sicher, wenn es das Design sauberer und besser macht als großartig, aber ich denke, das ist oft nicht der Fall.
Ich gehe davon aus, dass Ihre Unit-Tests so viel wie möglich bewirken würden, was möglicherweise keine 100% ige Abdeckung darstellt. In der Tat kann es nur 10% sein. Der Punkt ist, Ihre Unit-Tests sollten schnell sein und keine externen Abhängigkeiten haben. Sie können Fälle wie "Diese Methode löst eine ArgumentNullException aus, wenn Sie für diesen Parameter null übergeben" testen.
Ich würde dann Integrationstests hinzufügen (ebenfalls automatisiert und wahrscheinlich mit demselben Unit-Test-Framework), die externe Abhängigkeiten aufweisen können, und End-to-End-Szenarien wie diese testen.
Bei der Messung der Codeabdeckung messe ich sowohl Unit- als auch Integrationstests.
quelle
Es ist nichts Falsches daran, das Dateisystem zu treffen. Betrachten Sie es einfach als Integrationstest und nicht als Komponententest. Ich würde den fest codierten Pfad gegen einen relativen Pfad austauschen und einen TestData-Unterordner erstellen, der die Reißverschlüsse für die Komponententests enthält.
Wenn die Ausführung Ihrer Integrationstests zu lange dauert, trennen Sie sie, damit sie nicht so oft ausgeführt werden wie Ihre schnellen Komponententests.
Ich stimme zu, manchmal denke ich, dass interaktionsbasierte Tests zu viel Kopplung verursachen können und oft nicht genug Wert liefern. Sie möchten das Entpacken der Datei hier wirklich testen und nicht nur überprüfen, ob Sie die richtigen Methoden aufrufen.
quelle
Eine Möglichkeit wäre, die Entpackungsmethode zu schreiben, um InputStreams aufzunehmen. Dann könnte der Komponententest einen solchen InputStream aus einem Byte-Array unter Verwendung von ByteArrayInputStream erstellen. Der Inhalt dieses Byte-Arrays kann eine Konstante im Unit-Test-Code sein.
quelle
Dies scheint eher ein Integrationstest zu sein, da Sie von einem bestimmten Detail (dem Dateisystem) abhängen, das sich theoretisch ändern könnte.
Ich würde den Code, der sich mit dem Betriebssystem befasst, in ein eigenes Modul (Klasse, Assembly, JAR, was auch immer) abstrahieren. In Ihrem Fall möchten Sie eine bestimmte DLL laden, wenn sie gefunden wird. Erstellen Sie daher eine IDllLoader-Schnittstelle und eine DllLoader-Klasse. Lassen Sie Ihre App die DLL vom DllLoader über die Benutzeroberfläche abrufen und testen Sie, ob Sie nicht für den Entpackungscode verantwortlich sind, oder?
quelle
Angenommen, "Dateisysteminteraktionen" sind im Framework selbst gut getestet, erstellen Sie Ihre Methode für die Arbeit mit Streams und testen Sie diese. Das Öffnen und Übergeben eines FileStream an die Methode kann in Ihren Tests nicht berücksichtigt werden, da FileStream.Open von den Framework-Erstellern gut getestet wird.
quelle
Sie sollten die Klasseninteraktion und den Funktionsaufruf nicht testen. Stattdessen sollten Sie Integrationstests in Betracht ziehen. Testen Sie das gewünschte Ergebnis und nicht den Ladevorgang.
quelle
Für Unit-Tests würde ich vorschlagen, dass Sie die Testdatei in Ihr Projekt aufnehmen (EAR-Datei oder gleichwertig) und dann einen relativen Pfad in den Unit-Tests verwenden, dh "../testdata/testfile".
Solange Ihr Projekt korrekt exportiert / importiert wurde, sollte Ihr Komponententest funktionieren.
quelle
Wie andere gesagt haben, ist der erste als Integrationstest in Ordnung. Der zweite Test testet nur, was die Funktion tatsächlich tun soll, was alles ist, was ein Komponententest tun sollte.
Wie gezeigt, sieht das zweite Beispiel etwas sinnlos aus, bietet Ihnen jedoch die Möglichkeit zu testen, wie die Funktion auf Fehler in einem der Schritte reagiert. Sie haben im Beispiel keine Fehlerprüfung, aber im realen System, das Sie möglicherweise haben, und die Abhängigkeitsinjektion würde es Ihnen ermöglichen, alle Antworten auf Fehler zu testen. Dann haben sich die Kosten gelohnt.
quelle