UniTests 5: Dateien lesen

22. Juni 2012

Der letzte Beitrag hat die Verzeichnisstruktur eingeführt, die bei der Durchführung eines UnitTests entsteht. Und er hat gezeigt, wie ich die notwendigen Informationen erhalte um meine Daten an die richtige Stelle zu schreiben.

Diesmal geht es um die Frage, wie ich dem Test Dateien als Eingangsdaten mitgeben kann…

Hier die für diesen Beitrag relevante Verzeichnisstruktur:

image

Diesmal wird auch das ./bin Verzeichnis für den Compiler Output eine Rolle spielen. Wie bereits im letzten Beitrag ausgeführt ist das Verzeichnis ./Out für die Eingangsdaten des Tests zuständig (nicht etwa ./In). Und es ist normalerweise leer…

 

Dateien bereitstellen…

Bevor wir eine Datei einlesen können, sollten wir sie erst einmal haben. Und einen Test der sie verwendet.

Folgender Test liest eine Parameterdatei ein und verarbeitet sie:

   1: [TestMethod]

   2: public void TestSeveralUsersNotExisting()

   3: {

   4:     var inputFile = GetInputFileName1("TestWithFileAccess.TestSeveralUsersNotExisting.txt");

   5:  

   6:     var userNames = File.ReadAllLines(inputFile).Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToArray();

   7:     var data = new DataAccess();

   8:     foreach (var userName in userNames)

   9:     {

  10:         var user = data.GetUserInformation(userName);

  11:         Assert.IsNull(user, "User " + userName + " found unexpectedly!");

  12:     }

  13: }

Inhaltlich vielleicht etwas an den Haaren herbeigezogen, aber hier soll es nur darum gehen, wie man zu dieser Datei Zugriff erhält – im Grunde interessiert uns nur die erste Zeile: Wie kommt GetFileName1() an die Datei?

Schritt 1 ist zunächst das Anlegen der Datei im Testprojekt. Wie im letzten Beitrag für Ergebnisdateien beschrieben, mache ich auch bei Eingangsdateien gerne im Dateinamen klar, welchen Scope die Datei hat. Im vorliegenden Beispiel ist sie nur für einen Test relevant, also nutze ich {Testklasse}.{Testmethode}.{typ}:

image

Dateien die für mehrere Tests relevant sind sollte man natürlich inhaltlich benennen. Das sind in der Regel Dateien mit irgendwelchen Standard-Werten. 

Weiter ist wichtig, die Einstellung der Dateien in den Properties korrekt auf Content und Copy always zu setzen:

image

Nach einem Rebuild landen die Dateien damit neben den Assemblies im ./bin Ordner:

image

So weit, so gut.

 

No Deploy!

Lässt man den Test ohne weitere Vorkehrungen laufen, dann wird das Testassembly aus dem ./bin Ordner geladen. Eine erster Ansatz um die Datei zu finden könnte also über das Assembly gehen:

   1: private static string GetFileName1(string fileName)

   2: {

   3:     // otherwise it runs from the build directory:

   4:     string inputFile = Assembly.GetExecutingAssembly().Location;

   5:     inputFile = Path.GetDirectoryName(inputFile);

   6:     inputFile = Path.Combine(inputFile, fileName);

   7:     if (!File.Exists(inputFile))

   8:         Assert.Inconclusive("Input file " + fileName + " not found.");

   9:     return inputFile;

  10: }

Und in der Tat, das funktioniert recht gut, auch wenn es zunächst etwas umständlich erscheint.

Ein Vorteil dieser Methode ist, dass sie auch mit der nächsten Variante funktioniert, also unabhängig vom Vorgehen ist.

Schauen wir uns trotzdem die Alternative an…

 

Deploy!

Normalerweise hat man global für die Solution eine oder mehrere .testsettings Dateien, über die die Laufzeitumgebung für die Testdurchführung gesteuert wird. Zum Beispiel wird hier etwa Code Coverage eingeschaltet.

Eine der Einstellungen betrifft das Deployment, das man einschalten muss:

image

In diesem Dialog kann man zudem Dateien und ganze Verzeichnisse zum Deployment anmelden. (Global über alle Testprojekte hinweg. Nicht die beste Idee, die man haben kann.)

Resultat dieses Hakens ist, dass die Dateien aus dem ./bin Ordner nach ./Out kopiert werden. Zur Erinnerung: Dieses Verzeichnis ist im TestContext über das Property DeploymentDirectory erreichbar – nomen est omen.

Konsequenz ist auch, dass das Testassembly jetzt während eines Testlaufs nicht mehr aus dem ./bin Ordner sondern aus ./Out geladen und ausgeführt wird. Nur unsere Test-Dateien landen noch nicht dort, weshalb der Test zunächst fehlschlägt:

image

Was noch passieren muss, ist dass der Test die Datei zum Deployment anmeldet. Dazu gibt es ein Attribut, das auf der Testmethode oder Klasse angegeben werden kann:

   1: [TestMethod]

   2: [DeploymentItem("TestWithFileAccess.TestSeveralUsersExisting.txt")]

   3: public void TestSeveralUsersExisting()

Und schon klappt’s auch mit dem Nachbarn:

image

Allerdings wird immer noch die Variante über Assembly.Location verwendet. Nachdem wir uns nun voll im Kontext des Testframeworks befinden gibt es eine einfachere Variante:

   1: private string GetInputFileName2(string fileName)

   2: {

   3:     string inputFile = Path.Combine(this.TestContext.DeploymentDirectory, fileName);

   4:     if (!File.Exists(inputFile))

   5:         Assert.Inconclusive("Input file " + fileName + " not found.");

   6:     return inputFile;

   7: }

Allerdings funktioniert diese Variante nur bei eingeschaltetem Deployment, im Gegensatz zur ersten über Assembly.Location. Wer anfängt sich Hilfsklassen für UnitTests zu bauen, sollte also vermutlich beide Situationen berücksichtigen.

 

Eine Falle gibt es noch bei DeploymentItemAttribute: Hat man die zu deployende Datei im UnitTest-Projekt in einem Unterverzeichnis liegen, so muß man dieses explizit mit angeben. Will man dieses Unterverzeichnis auch im ./Out Ordner wiederfinden, muss auch das im Attribut angeben werden, ansonsten landet die Datei direkt in ./Out. Das Beispiel mit Unterverzeichnis ./NewFolder1:

   1: [TestMethod]

   2: [DeploymentItem(@"NewFolder1TestWithFileAccess.TestSeveralUsersExisting.txt", "NewFolder1")]

   3: public void TestSeveralUsersExisting()

   4: {

   5:     // read usernames from file

   6:     var inputFile = GetFileName2(@"NewFolder1TestWithFileAccess.TestSeveralUsersExisting.txt");

 

To Deploy Or Not To Deploy?

Zur Frage, ob man Deployment verwenden sollte oder nicht votiere ich ganz klar für Deployment, auch wenn es auf den ersten Blick umständlicher erscheint. Und zwar in der Variante mit DeploymentItemAttribute, nicht über die .testSettings-Datei. Wesentliche Vorteile sind vor allem:

  • Ich habe nach Durchführung im Testrun-Verzeichnis sowohl Eingangs- als auch Ausgangsdaten gesichert. Ein Verzicht auf Deployment würde mir die Eingangsdaten unterschlagen, so dass ich nicht nachträglich den Bezug herstellen kann, wenn die Eingangsdateien zwischenzeitlich im Projekt verändert wurden.
  • Die Testklasse oder Methode dokumentiert durch das Attribut ganz eindeutig ihre Abhängigkeiten. Weder die Verwendung der .testSettings-Datei, noch der Verzicht auf Deployment erreichen das.
  • Die .testsettings-Dateien – man hat oft genug mehr wie eine – sind global für die Solution, und deshalb sehr schlecht wartbar.

Dass bei der Verwendung des Attributes nur die Dateien kopiert werden, die für die gerade ausgeführten Tests relevant sind ist ein kleiner Bonus oben drauf.

 

Fazit

Tests bei denen Dateien verarbeitet werden müssen sind nicht ungewöhnlich. Ich bin aber schon einigen Entwickler begegnet, die froh waren, wenn das in den UnitTests einigermaßen funktionierte – ohne dass sie verstanden hätten warum. Schlimmer: Wenn es nicht funktionierte waren sie zunächst hilflos.

Mit diesem und dem vorigen Beitrag sollte die grundlegende Arbeitsweise des Umgangs mit Dateien in UnitTest klar genug sein, um damit sicher den nächsten Test schreiben zu können. Oder um festzustellen, warum ein Test seine Eingangsdateien nicht mehr findet.