web-dev-qa-db-de.com

Unit-Test-Datei-E/A

Beim Durchlesen der vorhandenen Threads zum Stapeltest, die sich auf Unit-Tests beziehen, konnte ich keine klare Antwort darauf finden, wie E/A-Vorgänge für Unit-Tests ausgeführt werden. Ich habe erst vor kurzem mit dem Komponententest begonnen, da ich mir der Vorteile zuvor bewusst war, aber Schwierigkeiten hatte, sich an das Schreiben von Tests zu gewöhnen. Ich habe mein Projekt so eingerichtet, dass es NUnit und Rhino Mocks verwendet, und obwohl ich das Konzept dahinter verstehe, habe ich Schwierigkeiten, die Verwendung von Mock-Objekten zu verstehen.

Ich habe zwei Fragen, die ich gerne beantworten würde. Erstens: Was ist der richtige Weg, um E/A-Vorgänge von Komponententests durchzuführen? Zweitens bin ich bei meinen Versuchen, etwas über Unit-Tests zu lernen, auf Abhängigkeit von Abhängigkeit gestoßen. Nach dem Einrichten und Arbeiten von Ninject habe ich mich gefragt, ob ich DI in meinen Unit-Tests verwenden sollte oder nur Objekte direkt instanziieren sollte.

56
Shaun Hamman

Check out Tutorial zu TDD mit Rhino Mocks und SystemWrapper .

SystemWrapper umschließt viele System.IO-Klassen, einschließlich File, FileInfo, Directory, DirectoryInfo, .... Sie können die vollständige Liste sehen .

In diesem Tutorial zeige ich, wie man mit MbUnit testet, aber es ist genau das gleiche für NUnit.

Ihr Test sieht ungefähr so ​​aus:

[Test]
public void When_try_to_create_directory_that_already_exists_return_false()
{
    var directoryInfoStub = MockRepository.GenerateStub<IDirectoryInfoWrap>();
    directoryInfoStub.Stub(x => x.Exists).Return(true);
    Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub));

    directoryInfoStub.AssertWasNotCalled(x => x.Create());
}
31
Vadim

Beim Testen des Dateisystems ist nicht unbedingt das Element one zu tun. In der Tat gibt es mehrere Dinge, die Sie je nach den Umständen tun können.

Die Frage, die Sie stellen müssen, lautet: Was teste ich?

  • Dass das Dateisystem funktioniert? Sie müssen that wahrscheinlich nicht testen, es sei denn, Sie verwenden ein Betriebssystem, mit dem Sie nicht vertraut sind. Wenn Sie beispielsweise einen Befehl zum Speichern von Dateien angeben, ist es Zeitverschwendung, einen Test zu schreiben, um sicherzustellen, dass sie wirklich speichern.

  • Dass die Dateien an der richtigen Stelle gespeichert werden? Nun, woher weißt du, was der richtige Ort ist? Vermutlich haben Sie Code, der einen Pfad mit einem Dateinamen kombiniert. Dies ist ein Code, den Sie leicht testen können: Ihre Eingabe besteht aus zwei Zeichenfolgen, und Ihre Ausgabe sollte eine Zeichenfolge sein, bei der es sich um einen gültigen Dateispeicherort handelt, der mit diesen beiden Zeichenfolgen erstellt wird.

  • Dass Sie die richtigen Dateien aus einem Verzeichnis bekommen? Sie müssen wahrscheinlich einen Test für Ihre File-Getter-Klasse schreiben, der das Dateisystem wirklich testet. Sie sollten jedoch ein Testverzeichnis mit Dateien verwenden, die sich nicht ändern. Sie sollten diesen Test auch in ein Integrationstestprojekt aufnehmen, da dies kein echter Komponententest ist, da er vom Dateisystem abhängt.

  • Aber ich muss etwas mit den Dateien machen, die ich bekomme. Für that test sollten Sie für Ihre File-Getter-Klasse ein fake verwenden. Ihre Fälschung sollte eine hartcodierte Liste von Dateien zurückgeben. Wenn Sie einen real -Datei-Getter und einen real -Dateiprozessor verwenden, wissen Sie nicht, welcher der Tests einen Fehler verursacht. Daher sollte Ihre Datei-Prozessor-Klasse beim Testen eine gefälschte Datei-Getter-Klasse verwenden. Ihre Datei-Prozessor-Klasse sollte den Datei-Getter interface annehmen. In echtem Code übergeben Sie den echten Datei-Getter. Im Testcode übergeben Sie einen gefälschten Datei-Getter, der eine bekannte, statische Liste zurückgibt.

Die grundlegenden Prinzipien sind:

  • Verwenden Sie ein hinter einer Schnittstelle verstecktes gefälschtes Dateisystem, wenn Sie das Dateisystem nicht selbst testen.
  • Wenn Sie echte Dateivorgänge testen müssen, dann
    • kennzeichnen Sie den Test als Integrationstest, nicht als Komponententest.
    • sie verfügen über ein festgelegtes Testverzeichnis, eine Reihe von Dateien usw., die sich immer in unverändertem Zustand befinden, sodass Ihre dateiorientierten Integrationstests konsistent bestehen können.
40
Ryan Lundy

Q1: 

Sie haben hier drei Möglichkeiten. 

Option 1: Mit ihm leben.  

(kein Beispiel: P)

Option 2: Erstellen Sie bei Bedarf eine leichte Abstraktion.  

Anstatt die Datei-E/A (File.ReadAllBytes oder was auch immer) in der zu testenden Methode auszuführen, können Sie sie so ändern, dass IO außerhalb ausgeführt wird und stattdessen ein Stream übergeben wird.

public class MyClassThatOpensFiles
{
    public bool IsDataValid(string filename)
    {
        var filebytes = File.ReadAllBytes(filename);
        DoSomethingWithFile(fileBytes);
    }
}

würde werden

// File IO is done outside prior to this call, so in the level 
// above the caller would open a file and pass in the stream
public class MyClassThatNoLongerOpensFiles
{
    public bool IsDataValid(Stream stream) // or byte[]
    {
        DoSomethingWithStreamInstead(stream); // can be a memorystream in tests
    }
}

Dieser Ansatz ist ein Kompromiss. Erstens, ja, es ist mehr überprüfbar. Es handelt sich jedoch um Testbarkeit, die die Komplexität geringfügig erhöht. Dies kann die Wartbarkeit und die Menge an Code beeinträchtigen, die Sie schreiben müssen, und Sie können Ihr Testproblem einfach um eine Stufe nach oben verschieben. 

Nach meiner Erfahrung ist dies jedoch ein schöner, ausgewogener Ansatz, da Sie die wichtige Logik verallgemeinern und überprüfbar machen können, ohne sich auf ein vollständig umschlossenes Dateisystem festzulegen. Das heißt Sie können die Bits verallgemeinern, die Ihnen wirklich wichtig sind, während Sie den Rest so belassen, wie er ist.

Option 3: Das gesamte Dateisystem umschließen

Wenn Sie noch einen Schritt weiter gehen, kann das Verspotten des Dateisystems einen gültigen Ansatz darstellen. Es hängt davon ab, mit wie viel Aufblähung Sie leben möchten. 

Ich bin diesen Weg schon einmal gegangen; Ich hatte eine umschlossene Dateisystem-Implementierung, aber am Ende habe ich es einfach gelöscht. Es gab subtile Unterschiede in der API, ich musste sie überall injizieren und letztendlich waren es zusätzliche Schmerzen für wenig Gewinn, da viele der Klassen, die sie benutzten, für mich nicht sehr wichtig waren. Wenn ich einen IoC-Container verwendet oder etwas geschrieben hatte, das kritisch war und die Tests schnell sein mussten, hätte ich vielleicht trotzdem daran festgehalten. Wie bei all diesen Optionen kann Ihre Laufleistung variieren.

Was Ihre IoC-Container-Frage betrifft:

Injizieren Sie Ihren Test manuell. Wenn Sie viele sich wiederholende Arbeiten durchführen müssen, verwenden Sie einfach Setup-/Factory-Methoden in Ihren Tests. Die Verwendung eines IoC-Containers zum Testen wäre extrem übertrieben! Vielleicht verstehe ich deine zweite Frage jedoch nicht.

10
Mark Simpson

Ich verwende das System.IO.Abstractions NuGet-Paket.

Diese Website enthält ein schönes Beispiel, das Ihnen zeigt, wie Sie die Injektion zum Testen verwenden. http://dontcodetired.com/blog/post/Unit-Testing-C-File-Access-Code-with-SystemIOAbstractions

Hier ist eine Kopie des Codes, der von der Website kopiert wurde.

using System.IO;
using System.IO.Abstractions;

namespace ConsoleApp1
{
    public class FileProcessorTestable
    {
        private readonly IFileSystem _fileSystem;

        public FileProcessorTestable() : this (new FileSystem()) {}

        public FileProcessorTestable(IFileSystem fileSystem)
        {
            _fileSystem = fileSystem;
        }

        public void ConvertFirstLineToUpper(string inputFilePath)
        {
            string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");

            using (StreamReader inputReader = _fileSystem.File.OpenText(inputFilePath))
            using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
            {
                bool isFirstLine = true;

                while (!inputReader.EndOfStream)
                {
                    string line = inputReader.ReadLine();

                    if (isFirstLine)
                    {
                        line = line.ToUpperInvariant();
                        isFirstLine = false;
                    }

                    outputWriter.WriteLine(line);
                }
            }
        }
    }
}





using System.IO.Abstractions.TestingHelpers;
using Xunit;

namespace XUnitTestProject1
{
    public class FileProcessorTestableShould
    {
        [Fact]
        public void ConvertFirstLine()
        {
            var mockFileSystem = new MockFileSystem();

            var mockInputFile = new MockFileData("line1\nline2\nline3");

            mockFileSystem.AddFile(@"C:\temp\in.txt", mockInputFile);

            var sut = new FileProcessorTestable(mockFileSystem);
            sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");

            MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");

            string[] outputLines = mockOutputFile.TextContents.SplitLines();

            Assert.Equal("LINE1", outputLines[0]);
            Assert.Equal("line2", outputLines[1]);
            Assert.Equal("line3", outputLines[2]);
        }
    }
}
2
Tony

Derzeit verbrauche ich ein IFileSystem-Objekt über Abhängigkeitseinspritzung. Für Produktionscode implementiert eine Wrapper-Klasse die Schnittstelle, indem sie spezifische IO - Funktionen umgibt, die ich benötige. Beim Testen kann ich eine Null- oder Stub-Implementierung erstellen und diese der getesteten Klasse zur Verfügung stellen. Die getestete Klasse ist nicht klüger.

1
Grant Palin

Seit 2012 können Sie dies mithilfe von Microsoft Fakes durchführen, ohne die Codebase beispielsweise ändern zu müssen, da sie bereits eingefroren wurde.

Zuerst erzeugen Sie eine falsche Assembly für System.dll - oder ein beliebiges anderes Paket und mock dann die erwarteten Ergebnisse wie in:

using Microsoft.QualityTools.Testing.Fakes;
...
using (ShimsContext.Create())
{
     System.IO.Fakes.ShimFile.ExistsString = (p) => true;
     System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content";

      //Your methods to test
}