it-swarm.com.de

Komponententestcode mit einer Dateisystemabhängigkeit

Ich schreibe eine Komponente, die angesichts einer Zip-Datei Folgendes muss:

  1. Entpacke die Datei.
  2. Suchen Sie eine bestimmte DLL unter den entpackten Dateien.
  3. Laden Sie diese DLL durch Reflektion und rufen Sie eine Methode auf.

Ich würde diese Komponente gerne einmal testen.

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 sich auf das Dateisystem, die Datenbank, das Netzwerk usw. stützen."

Wenn ich dies in einer Unit-Test-freundlichen Art und 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 Test-Doubles (Mocks) in die DoIt-Methode einspeisen. 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 geprüft, 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 ist dies: Wie kann man einen vom Dateisystem abhängigen Unit-Test durchführen?

edit Ich verwende .NET, aber das Konzept könnte auch Java oder nativen Code anwenden.

128

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 keine unbeabsichtigten Nebenwirkungen auftreten, wenn Sie mit dem Dateisystem interagieren. Stellen Sie insbesondere sicher, dass Sie nach dem Bereinigen die von Ihnen erstellten temporären Dateien löschen und nicht versehentlich eine vorhandene Datei überschreiben, die zufällig denselben Dateinamen wie die von Ihnen verwendete temporäre Datei hat. Verwenden Sie immer relative Pfade und keine absoluten Pfade.

Es wäre auch eine gute Idee, chdir() in ein temporäres Verzeichnis zu kopieren, bevor Sie Ihren Test ausführen, und chdir() danach zurück.

42
Adam Rosenfield

Yay! Jetzt ist es testbar; Ich kann Test-Doubles (Mocks) in die DoIt-Methode einspeisen. 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 geprüft, ob die Zip-Datei ordnungsgemäß entpackt wurde usw.

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 brauchen nicht zu testen (in diesem Unit-Test), ob eine Datei korrekt entpackt wurde, Ihre Methode hält dies für selbstverständlich. Die Schnittstellen sind für sich genommen wertvoll, da sie Abstraktionen bieten, gegen die Sie programmieren können, anstatt sich implizit oder explizit auf eine konkrete Implementierung zu verlassen.

64
andreas buykx

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 zusammenfügt. Wenn Sie also einen Komponententest dafür schreiben, müssen Sie lediglich behaupten, dass Methoden aufgerufen wurden. Tests wie diese verbinden Ihre Implementierungsdetails eng mit dem Test. Das ist schlecht, weil Sie jetzt den Test jedes Mal ändern müssen, wenn Sie die Implementierungsdetails Ihrer Methode ändern, weil das Ändern der Implementierungsdetails Ihre Tests bricht!

Schlechte Tests sind schlimmer als gar keine.

In deinem Beispiel:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Während Sie Mocks übergeben können, enthält die zu testende Methode keine Logik. Wenn Sie versuchen, einen Komponententest durchzuführen, könnte dies ungefähr so ​​aussehen:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Herzlichen Glückwunsch, Sie haben im Grunde genommen die Implementierungsdetails Ihrer DoIt() -Methode in einen Test kopiert. Viel Spaß beim Unterhalten.

Wenn Sie Tests schreiben, möchten Sie die [~ # ~] was [~ # ~] testen und nicht die [~ # ~] wie [~ # ~]. Siehe Black Box Testing für mehr.

Das [~ # ~] was [~ # ~] ist der Name Ihrer Methode (oder sollte es zumindest sein). Die [~ # ~] wie [~ # ~] sind all die kleinen Implementierungsdetails, die in Ihrer Methode vorkommen. Bei guten Tests können Sie das [~ # ~] wie [~ # ~] austauschen, ohne das [ ~ # ~] was [~ # ~] .

Denken Sie so darüber nach, fragen Sie sich:

"Wenn ich die Implementierungsdetails dieser Methode ändere (ohne den öffentlichen Auftrag zu ändern), brechen meine Tests dadurch ab?"

Wenn die Antwort ja ist, testen Sie das [~ # ~] wie [~ # ~] und nicht das [~ # ~] was [~ # ~] .

Um Ihre spezifische Frage zum Testen von Code mit Dateisystemabhängigkeiten zu beantworten, nehmen wir an, Sie hatten etwas Interessanteres mit einer Datei zu tun und Sie wollten den Base64-codierten Inhalt eines byte[] In einer Datei speichern. Sie können dafür Streams verwenden, um zu testen, ob Ihr Code das Richtige tut, ohne zu überprüfen , wie er es tut. Ein Beispiel könnte so etwas sein (in Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Der Test verwendet ein ByteArrayOutputStream, aber in der Anwendung (mit Abhängigkeitsinjektion) würde die echte StreamFactory (möglicherweise FileStreamFactory genannt) FileOutputStream von outputStream() zurückgeben und in a schreiben File.

Das Interessante an der write -Methode war, dass sie den Inhalt in Base64-Codierung geschrieben hat, also haben wir das getestet. Für Ihre DoIt() -Methode würde dies geeigneter mit einem Integrationstest getestet.

50

Ich bin zurückhaltend, meinen Code mit Typen und Konzepten zu verschmutzen, die nur existieren, um das Testen von Einheiten zu erleichtern. Klar, wenn es das Design sauberer und besser macht, dann großartig, aber ich denke, das ist oft nicht der Fall.

Ich gehe davon aus, dass Ihre Unit-Tests so viel wie möglich leisten würden, was möglicherweise nicht 100% Deckung bedeutet. In der Tat kann es nur 10% sein. Der Punkt ist, Ihre Unit-Tests sollten schnell sein und keine externen Abhängigkeiten aufweisen. 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 solche End-to-End-Szenarien testen.

Bei der Messung der Codeabdeckung messe ich sowohl Unit- als auch Integrationstests.

23
Kent Boogaart

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 durch einen relativen Pfad ersetzen und einen TestData-Unterordner erstellen, der die Reißverschlüsse für die Komponententests enthält.

Wenn Ihre Integrationstests zu lange dauern, trennen Sie sie voneinander, damit sie nicht so oft ausgeführt werden wie Ihre schnellen Unit-Tests.

Ich stimme zu, manchmal denke ich, dass interaktionsbasiertes Testen zu viel Kopplung verursachen kann und am Ende oft nicht genügend Wert liefert. Sie möchten die Datei unbedingt hier entpacken und nicht nur überprüfen, ob Sie die richtigen Methoden aufrufen.

8
JC.

Eine Möglichkeit wäre, die Entpackungsmethode zu schreiben, um InputStreams zu verwenden. Dann könnte der Komponententest einen solchen InputStream mit ByteArrayInputStream aus einem Byte-Array erstellen. Der Inhalt dieses Bytearrays könnte eine Konstante im Einheitentestcode sein.

6
nsayer

Dies scheint eher ein Integrationstest zu sein, da Sie theoretisch von einem bestimmten Detail (dem Dateisystem) abhängen, das sich ändern könnte.

Ich würde den Code, der sich mit dem Betriebssystem befasst, in ein eigenes Modul abstrahieren (Klasse, Assembly, Jar, was auch immer). In Ihrem Fall möchten Sie eine bestimmte DLL, falls vorhanden, erstellen Sie eine IDllLoader-Schnittstelle und eine DllLoader-Klasse. Lassen Sie Ihre App die DLL vom DllLoader verwenden die schnittstelle testen und das .. bist du doch nicht für den entpackungscode verantwortlich oder?

3
tap

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 FileStreams an die Methode kann aus Ihren Tests ausgeschlossen werden, da FileStream.Open von den Framework-Erstellern gut getestet wird.

2
Sunny Milenov

Sie sollten die Klasseninteraktion und den Funktionsaufruf nicht testen. stattdessen sollten Sie Integrationstests in Betracht ziehen. Testen Sie das erforderliche Ergebnis und nicht den Dateiladevorgang.

1
Dror Helper

Für Unit-Tests würde ich vorschlagen, dass Sie die Testdatei in Ihr Projekt aufnehmen (EAR-Datei oder gleichwertiges) und dann einen relativen Pfad in den Unit-Tests verwenden, d. H. "../Testdata/testfile".

Solange Ihr Projekt korrekt exportiert/importiert wird, sollte Ihr Komponententest funktionieren.

1
James Anderson

Wie andere gesagt haben, ist der erste Test als Integrationstest in Ordnung. Der zweite Test testet nur, was die Funktion eigentlich tun soll, was alles ein Unit-Test 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 möglicherweise, und mit der Abhängigkeitsinjektion können Sie alle Antworten auf Fehler testen. Dann haben sich die Kosten gelohnt.

0
David Sykes