UnitTests 3: Voraussetzungen prüfen!

15. Juni 2012

Wer kennt das nicht: Man steigt in ein laufendes Projekt ein, setzt einen neuen Rechner auf oder kommt aus dem Urlaub… und alle Tests – oder eine große Teilmenge – sind rot.

Völlig klar, da fehlt eine Installation. Oder ein Connection-String ist nicht gesetzt. Berechtigungsproblem? Oder hat nicht vielleicht doch die letzte Änderung gravierendere Auswirkungen gehabt, als gedacht?

Jeder kennt diese Situation, und jeder kennt auch die Lösung: Compiler fragen. Debugger fragen. Kollegen fragen. Kurz: Klassische Fehlerdiagnose.

Das geht auch anders…

Tatsächlich ist es hier so, dass die Tests nicht rot sind, weil der Test selbst schief gegangen ist. Sie sind rot, weil die Voraussetzungen für die Ausführung des Tests nicht erfüllt sind…

Ich möchte an dieser Stelle nochmal betonen, dass ich für diese Beiträge den Begriff “UnitTest” sehr lax interpretiere und damit lediglich Tests meine, die MSTest als UnitTest-Framework nutzen. (Ungenannt bleibende Kollegen mögen also bitte von Grundsatzdiskussionen absehen Winking smile.)
 

Die Testvorraussetzungen prüfen

Um es konkreter zu machen ein einfaches Beispiel:

   1: [TestMethod]

   2: public void TestGetBalanceSheets()

   3: {

   4:     var data = new DataAccess();

   5:  

   6:     // we know the test user and ID!

   7:     var result = data.GetBalanceSheets(100);

   8:  

   9:     // test user always has 2 balance sheets!

  10:     Assert.IsTrue(result.Count() == 2);

  11: }

Gestern lief der Test noch, heute bekomme ich folgendes Ergebnis:

image

Eine Exception? Hat jemand die Methode geändert, die ich aufrufe? Wirft diese eine Exception? Oder liefert sie null zurück? … Selbst bei diesem einfachen Beispiel kann man schon auf genügend falsche Fährten gelockt werden. Dabei ist die Lösung so einfach: Der Datenbank-Server ist umgezogen und der Connection-String im Unittest-Projekt wurde noch nicht angepasst.

Hätte er mir das nicht auch gleich verraten können? Hätte er!

Man tut sich deutlich leichter solche systemischen Probleme schnell zu erkennen und zu beheben, wenn man die Voraussetzungen für den Test prüft. Und wenn man einen Fehler an dieser Stelle dann noch per Assert.Inconclusive() meldet, verrät einem alleine die Farbe in der Ausgabe schon, wo das Problem liegt.

Im Beispiel reicht eine simple Prüfung, ob die Datenbank erreichbar ist:

   1: private void AssertDatabaseAvailable()

   2: {

   3:     try

   4:     {

   5:         var data = new DataAccess();

   6:         // we may not find anything, but we should be able to read!

   7:         var actual = data.GetUserInformation("does not matter");

   8:     }

   9:     catch (SqlException)

  10:     {

  11:         Assert.Inconclusive("Database could not be reached!");

  12:     }

  13: }

Diese kann man nun in jeder Testmethode aufrufen – oder man macht das einmal global für die Testklasse:

   1: [TestInitialize]

   2: public void TestInitialize()

   3: {

   4:    AssertDatabaseAvailable();

   5: }

Und schon sieht das Resultat des Testlaufs ganz anders aus:

image

Inconclusive? Also kein echter Fehler sondern ein systemische Problem. (Erste Reaktion: Durchatmen – besonders wenn wir über dutzende oder mehr Fehler reden.) Datenbank nicht erreichbar? Gut, da weiß ich, wo ich hinschauen muss…

 

 

Weitere Beispiele

Der Connection-String mag ein einfaches (weil vergleichsweise offensichtliches) Problem sein. Hier ein anderes, weniger offensichtliches Beispiel: Ein Test ob das dynamische Nachladen von Assemblies funktioniert.

   1: [TestMethod]

   2: public void TestLoadModuleAssembly()

   3: {

   4:     var assembly = ModulKatalog.FindAndLoadAssembly("Privatbilanz.Module.Auswertungen");

   5:     Assert.IsNotNull(assembly);

   6: }

Der Testlauf ist wie er sein soll:

image

Also alles gut? Getestet werden soll ja offensichtlich, ob ein Assembly gesucht, gefunden und dynamisch geladen werden kann. Aber was, wenn es vorher schon geladen war?

Tatsächlich macht dieser Test nur Sinn, wenn das Assembly vorher noch nicht geladen ist, denn nur dann findet das Suchen, Finden und Laden überhaupt statt. So wie der Test jetzt aussieht, wiegt er uns womöglich in falscher Sicherheit.

Also bauen wir eine kleine Prüfmethode ein:

   1: static void AssertAssemblyNotLoaded(string name)

   2: {

   3:     var loaded = AppDomain.CurrentDomain

   4:         .GetAssemblies()

   5:         .Where(a => a.GetName().Name == name)

   6:         .Any();

   7:     if (loaded)

   8:         Assert.Inconclusive("Assembly " + name + " already loaded!");

   9: }

Und rufen sie im Test auf:

   1: [TestMethod]

   2: public void TestLoadModuleAssembly()

   3: {

   4:     AssertAssemblyNotLoaded("Privatbilanz.Module.Auswertungen");

   5:     var assembly = ModulKatalog.FindAndLoadAssembly("Privatbilanz.Module.Auswertungen");

   6:     Assert.IsNotNull(assembly);

   7: }

Und siehe da:

image33

Der Test ist zwar sicher ursprünglich korrekt gelaufen, aber zwischenzeitlich hat wohl jemand mit irgendwelchen Änderungen dafür gesorgt, dass das Assembly schon vorher geladen wird. Das hatte einen Seiteneffekt auf unseren Test, der zumindest verhindert, dass getestet wird, was wir eigentlich im Sinn hatten. Das muß noch nicht heißen, dass da jetzt ein Fehler vorliegt, aber wenn das so wäre, würden wir ihn ohne diese Prüfung nicht mehr entdecken, grüner Test hin oder her.

 

Weitere Beispiele solcher Seiteneffekte sind globale Objekte (Singletons), deren Initialisierung getestet werden soll, oder von deren Zustand unser Test abhängt. Außerdem natürlich auch weitere Infrastruktur-Dinge, wie Zugriff auf Dateien, das EventLog, Services, und so weiter.

Typisches Warnzeichen, dass hier etwas im Argen liegt ist übrigens, dass Tests in Isolation wunderbar laufen, aber in Kombination mit anderen Tests plötzlich fehlschlagen.

 

Fazit

Das explizite Prüfen der Testvoraussetzungen hilft zum einen systemische Fehler schneller zu erkennen. Damit hilft es, die Fehlermeldung schnell einzuordnen und man verschwendet nicht unnötig Zeit mit der Fehlerdiagnose. Zum anderen kann es – wie gezeigt – “false positives” verhindern, so dass man nicht in falscher Sicherheit gewiegt wird.