wake-up-neo.com

Wie teste ich die Integration in .NET mit echten Dateien?

Ich habe einige Klassen, die eine Logik im Zusammenhang mit Dateisystemen und Dateien implementieren. Im Rahmen dieser Logik führe ich beispielsweise folgende Aufgaben aus:

  • überprüfen, ob ein bestimmter Ordner eine bestimmte Struktur hat (z. B. enthält er Unterordner mit bestimmten Namen usw.)
  • laden einiger Dateien aus diesen Ordnern und Überprüfen ihrer Struktur (z. B. sind dies einige Konfigurationsdateien, die sich an einer bestimmten Stelle in einem bestimmten Ordner befinden)
  • lade zusätzliche Dateien zum Testen/Validieren aus der Konfigurationsdatei (zB diese Konfigurationsdatei enthält Informationen über andere Dateien im selben Ordner, die eine andere interne Struktur haben sollten etc ...)

Jetzt hat all diese Logik einen gewissen Workflow und es werden Ausnahmen geworfen, wenn etwas nicht stimmt (z. B. Konfigurationsdatei wird nicht am spezifischen Ordnerort gefunden). Darüber hinaus ist Managed Extensibility Framework (MEF) an dieser Logik beteiligt, da einige dieser Dateien, die ich überprüfe, verwaltete DLLs sind, die ich manuell in MEF-Aggregate usw. lade.

Jetzt möchte ich das alles auf irgendeine Weise testen. Ich dachte daran, mehrere physische Testordner auf der Festplatte zu erstellen, die verschiedene Testfälle abdecken, und dann meinen Code für sie auszuführen. Ich könnte zum Beispiel erstellen:

  • ordner mit korrekter Struktur und allen gültigen Dateien
  • ordner mit korrekter Struktur, aber mit ungültiger Konfigurationsdatei
  • ordner mit korrekter Struktur aber fehlender Konfigurationsdatei etc ...

Wäre das der richtige Ansatz? Ich bin mir jedoch nicht sicher, wie ich meinen Code in diesem Szenario genau ausführen soll. Ich möchte auf keinen Fall die gesamte Anwendung ausführen und darauf verweisen, um diese verspotteten Ordner zu überprüfen. Sollte ich ein Unit-Test-Framework verwenden, um Art von "Unit-Tests" zu schreiben, die meinen Code für diese Dateisystemobjekte ausführen?

Ist all dies im Allgemeinen ein korrekter Ansatz für diese Art von Testszenarien? Gibt es andere bessere Ansätze?

54
matori82

Zuallererst, denke ich, ist es besser, Komponententests zu schreiben, um Ihre Logik zu testen, ohne externe Ressourcen zu berühren. Hier haben Sie zwei Möglichkeiten:

  1. sie müssen die Abstraktionsschicht verwenden, um Ihre Logik von externen Abhängigkeiten wie dem Dateisystem zu isolieren. Sie können diese Abstraktionen in Komponententests problemlos stub- oder verspotten (von Hand oder mithilfe eines eingeschränkten Isolationsframeworks wie NSubstitute, FakeItEasy oder Moq). Ich bevorzuge diese Option, weil in diesem Fall Tests Sie zu einem besseren Design führen.
  2. wenn Sie mit altem Code umgehen müssen (nur in diesem Fall), können Sie eines der uneingeschränkten Isolations-Frameworks (wie TypeMock Isolator, JustMock oder Microsoft Fakes) verwenden, das so ziemlich alles (zum Beispiel versiegelt und statisch) verspotten kann Klassen, nicht virtuelle Methoden). Aber sie kosten Geld. Die einzige "kostenlose" Option ist Microsoft Fakes, es sei denn, Sie sind der glückliche Besitzer von Visual Studio 2012/2013 Premium/Ultimate.

In Unit-Tests müssen Sie die Logik externer Bibliotheken wie MEF nicht testen.

Zweitens, wenn Sie Integrationstests schreiben möchten, müssen Sie einen "Happy Path" -Test (wenn alles in Ordnung ist) und einige Tests schreiben, die Ihre Logik in Grenzfällen testen (Datei oder Verzeichnis nicht gefunden). Im Gegensatz zu @Sergey Berezovskiy empfehle ich, separate Ordner für jeden Testfall zu erstellen. Die Hauptvorteile sind:

  1. sie können Ihrem Ordner aussagekräftige Namen geben, die Ihre Absichten deutlicher zum Ausdruck bringen.
  2. sie müssen keine komplexe (d. h. zerbrechliche) Setup-/Teardown-Logik schreiben.
  3. auch wenn Sie sich später für eine andere Ordnerstruktur entscheiden, können Sie diese einfacher ändern, da Sie bereits über Arbeitscode und Tests verfügen (das Umgestalten unter Test Harness ist viel einfacher).

Für Unit- und Integrationstests Sie können normale Unit-Test-Frameworks verwenden (wie NUnit oder xUnit.NET). Mit diesem Framework ist es ziemlich einfach, Ihre Tests in Continuous-Integrationsszenarien auf Ihrem Build-Server zu starten.

Wenn Sie sich entscheiden, beide Arten von Tests zu schreiben, dann Sie müssen Unit-Tests von Integrationstests trennen (Sie können für jede Art von Tests separate Projekte erstellen). Gründe dafür:

  1. Komponententests ist ein Sicherheitsnetz für Entwickler. Sie müssen eine schnelle Rückmeldung über das erwartete Verhalten der Systemeinheiten nach den letzten Codeänderungen (Fehlerkorrekturen, neue Funktionen) geben. Wenn sie häufig ausgeführt werden, kann der Entwickler schnell und einfach einen Teil des Codes identifizieren, der das System beschädigt hat. Niemand möchte langsame Unit-Tests durchführen.
  2. Integrationstests sind in der Regel langsamer als Unit-Tests. Aber sie haben einen anderen Zweck. Sie prüfen, ob die Einheiten mit tatsächlichen Abhängigkeiten wie erwartet funktionieren.
59
Vladimir Almaev

Sie sollten mit Unit-Tests so viel Logik wie möglich testen, indem Sie Aufrufe an das Dateisystem hinter Schnittstellen abstrahieren. Durch die Verwendung von Dependency Injection und einem Testframework wie FakeItEasy können Sie testen, ob Ihre Schnittstellen tatsächlich zur Bearbeitung der Dateien und Ordner verwendet/aufgerufen werden.

Irgendwann müssen Sie jedoch die Implementierungen testen, die auch auf dem Dateisystem funktionieren, und hier müssen Sie Integrationstests durchführen.

Die Dinge, die Sie testen müssen, scheinen relativ isoliert zu sein, da Sie nur Ihre eigenen Dateien und Verzeichnisse auf Ihrem eigenen Dateisystem testen möchten. Wenn Sie eine Datenbank oder ein anderes externes System mit mehreren Benutzern usw. testen möchten, sind die Dinge möglicherweise komplizierter.

Ich glaube nicht, dass es "offizielle Regeln" dafür gibt, wie man solche Integrationstests am besten durchführt, aber ich glaube, Sie sind auf dem richtigen Weg. Einige Ideen, auf die Sie hinarbeiten sollten:

  • Klare Standards: Machen Sie die Regeln und den Zweck jedes Tests absolut klar.
  • Automatisierung: Die Möglichkeit, Tests schnell und ohne zu viele manuelle Anpassungen erneut auszuführen.
  • Wiederholbarkeit: Eine Testsituation, die Sie "zurücksetzen" können, so dass Sie Tests mit nur geringen Abweichungen schnell wiederholen können.

Erstellen Sie ein wiederholbares Testszenario

In Ihrer Situation würde ich zwei Hauptordner einrichten: Einen, in dem alles so ist, wie es sein soll (d. H. Richtig funktioniert), und einen, in dem alle Regeln verletzt sind.

Ich würde diese Ordner und alle Dateien in ihnen erstellen, dann jeden der Ordner komprimieren und Logik in eine Testklasse schreiben, um jeden von ihnen zu dekomprimieren.

Dies sind keine wirklichen Tests; Stellen Sie sich diese stattdessen als "Skripte" zum Einrichten Ihres Testszenarios vor, mit denen Sie Ihre Ordner und Dateien einfach und schnell löschen und neu erstellen können, selbst wenn Ihre Hauptintegrationstests sie während des Tests ändern oder durcheinander bringen sollten. Der Grund, warum Sie sie in eine Testklasse einordnen, besteht darin, dass Sie sie einfach über dieselbe Schnittstelle ausführen können, mit der Sie während des Tests arbeiten werden.

Testen

Erstellen Sie zwei Sätze von Testklassen, einen für jede Situation (Richten Sie Ordner gegen Ordner mit fehlerhaften Regeln richtig ein). Platzieren Sie diese Tests in einer Ordnerhierarchie, die für Sie von Bedeutung ist (abhängig von der Komplexität Ihrer Situation).

Es ist nicht klar, wie gut Sie mit Unit-/Integrationstests vertraut sind. In jedem Fall würde ich NUnit empfehlen. Ich verwende die Erweiterungen auch gerne in Should. Beides erhalten Sie bei Nuget:

install-package Nunit
install-package Should

Mit dem Paket should können Sie den Testcode folgendermaßen schreiben:

someCalculatedIntValue.ShouldEqual(3); 
someFoundBoolValue.ShouldBeTrue();

Beachten Sie, dass mehrere Testläufer verfügbar sind, mit denen Sie Ihre Tests durchführen können. Ich persönlich habe nur echte Erfahrungen mit dem in Resharper eingebauten Läufer gemacht, bin aber sehr zufrieden und kann ihn ohne Probleme weiterempfehlen.

Unten sehen Sie ein Beispiel einer einfachen Testklasse mit zwei Tests. Beachten Sie, dass wir im ersten Fall mit einer Erweiterungsmethode von Should nach einem erwarteten Wert suchen, während wir im zweiten Fall nichts explizit testen. Dies liegt daran, dass es mit [ExpectedException] markiert ist, was bedeutet, dass es fehlschlägt, wenn beim Ausführen des Tests keine Ausnahme des angegebenen Typs ausgelöst wird. Sie können dies verwenden, um zu überprüfen, ob eine entsprechende Ausnahme ausgelöst wird, wenn eine Ihrer Regeln verletzt wird.

[TestFixture] 
public class When_calculating_sums
{                    
    private MyCalculator _calc;
    private int _result;

    [SetUp] // Runs before each test
    public void SetUp() 
    {
        // Create an instance of the class to test:
        _calc = new MyCalculator();

        // Logic to test the result of:
        _result = _calc.Add(1, 1);
    }

    [Test] // First test
    public void Should_return_correct_sum() 
    {
        _result.ShouldEqual(2);
    }

    [Test] // Second test
    [ExpectedException(typeof (DivideByZeroException))]
    public void Should_throw_exception_for_invalid_values() 
    {
        // Divide by 0 should throw a DivideByZeroException:
        var otherResult = _calc.Divide(5, 0);
    }       

    [TearDown] // Runs after each test (seldom needed in practice)
    public void TearDown() 
    {
        _calc.Dispose(); 
    }
}

Mit all dem sollten Sie in der Lage sein, Testszenarien zu erstellen, neu zu erstellen und Tests auf einfache und wiederholbare Weise durchzuführen.


Bearbeiten: Wie in einem Kommentar ausgeführt, ist Assert.Throws () eine weitere Option , um sicherzustellen, dass Ausnahmen wie erforderlich ausgelöst werden. Persönlich mag ich die Tag-Variante, und mit Parametern , Sie können dort auch Dinge wie die Fehlermeldung überprüfen. Ein weiteres Beispiel (unter der Annahme, dass eine benutzerdefinierte Fehlermeldung von Ihrem Rechner ausgegeben wird):

[ExpectedException(typeof(DivideByZeroException), ExpectedMessage="Attempted to divide by zero" )]
public void When_attempting_something_silly(){  
    ...
}
8
Kjartan

Ich würde mit einem einzigen Testordner gehen. Für verschiedene Testfälle können Sie im Rahmen der Kontexteinstellung verschiedene gültige/ungültige Dateien in diesen Ordner kopieren. Im Test-Teardown einfach diese Dateien aus dem Ordner entfernen.

Z.B. mit Specflow :

Given configuration file not exist
When something
Then foo

Given configuration file exists
And some dll not exists
When something
Then bar

Definieren Sie jeden Schritt zum Einrichten des Kontexts als Kopieren/Nicht-Kopieren der entsprechenden Datei in Ihren Ordner. Sie können auch Tabelle verwenden, um festzulegen, welche Datei in den Ordner kopiert werden soll:

Given some scenario
| FileName         |
| a.config         |
| b.invalid.config |
When something
Then foobar
3

Ich kenne die Architektur Ihres Programms nicht, um einen guten Rat zu geben, aber ich werde es versuchen

  1. Ich glaube, dass Sie keine echte Dateistruktur testen müssen. Dateizugriffsdienste werden vom System/Framework definiert und müssen nicht getestet werden. Sie müssen diese Dienste in verwandten Tests verspotten.
  2. Außerdem müssen Sie MEF nicht testen. Es ist bereits getestet.
  3. Verwenden Sie SOLID-Prinzipien, um Komponententests durchzuführen. Betrachten Sie insbesondere Prinzip der Einzelverantwortung, um Komponententests zu erstellen, die nicht miteinander in Beziehung stehen. Vergiss nur nicht, dich zu lustig zu machen, um Abhängigkeiten zu vermeiden.
  4. Um Integrationstests durchzuführen, können Sie eine Reihe von Hilfsklassen erstellen, die Szenarien von Dateistrukturen emulieren, die Sie testen möchten. Auf diese Weise können Sie nicht an den Computer gebunden bleiben, auf dem Sie diese Tests ausführen. Ein solcher Ansatz ist vielleicht komplizierter als die Erstellung einer echten Dateistruktur, aber ich mag es.
1
Max

Ich würde Framework-Logik erstellen und Parallelitätsprobleme und Dateisystemausnahmen testen, um eine gut definierte Testumgebung sicherzustellen.

Versuchen Sie, alle Grenzen der Problemdomäne aufzulisten. Wenn es zu viele gibt, sollten Sie die Möglichkeit in Betracht ziehen, dass Ihr Problem zu weit gefasst ist und aufgeschlüsselt werden muss. Was sind die vollständigen erforderlichen und ausreichenden Bedingungen, damit Ihr System alle Tests besteht? Betrachten Sie dann jeden Zustand und behandeln Sie ihn als individuellen Angriffspunkt. Und liste alle Möglichkeiten auf, die dir einfallen, wenn du dagegen verstößt. Versuchen Sie, sich selbst zu beweisen, dass Sie alle gefunden haben. Dann schreiben Sie jeweils einen Test.

Ich würde den obigen Prozess zuerst für die Umgebung durchgehen, diesen erst zu einem zufriedenstellenden Standard erstellen und testen und dann für die detailliertere Logik innerhalb des Workflows. Möglicherweise ist eine gewisse Iteration erforderlich, wenn beim Testen Abhängigkeiten zwischen der Umgebung und der detaillierten Logik auftreten.

0
Brad Thomas