HowTo: Einfache Tests – UnitTests (oder keine Angst vor UnitTests…)

Einführung

Das Konzept der UnitTests (es gibt noch ein paar weitere Formen) ist bereits seit etlichen Jahren (oder Jahrzehnten?) bekannt. Es gibt viele Testframeworks für fast jede Sprache.
Im Visual Studio 2008 (jedenfalls der Professional/Team System Edition) sind UnitTests sehr einfach zu erstellen – und trotzdem hab ich erst vor kurzem die UnitTests für mich entdeckt. Trotz der Gründe für UnitTests und der Einfachheit kenn ich etliche Projekte, wo diese nicht existieren oder angewendet werden.
Die meisten Entwickler denken, dass UnitTests ziemlich komplex sind und eigentlich unnötig.
Die Gründe der Entwickler sind vielfältig (auch ich hab früher so oder so ähnlich gedacht ;) ) :

  • “Warum nicht einfach ein Konsolenprogramm erstellen oder per Debugger prüfen?”
  • “Ich seh es doch wenn eine bestimmte Komponente nicht funktioniert.”
  • “Für den extra Aufwand hab ich leider keine Zeit.”
  • “UnitTests klingt doch recht kompliziert, da ist mir die Einarbeitungszeit zu hoch.”

Ich bin kein Experte in UnitTests, allerdings haben sie mich bereits nach wenigen Minuten begeistert :)

UnitTests sind sehr schnell gemacht

(Achtung: Diese Aussage nicht auf die Goldwaage legen. Gute und durchdachte UnitTests sind keine leichte Aufgabe – darum gibt es ja z.B. auch eine extra Test Edition von Visual Studio wo sich)
Aber für den Anfang wollen wir die Behauptung mal so stehen lassen – Einfache Tests können sehr schnell durchgeführt werden)

Im Visual Studio 2008 wurde ein extra Template für Tests bereitgestellt:

image

Zudem kann man direkt in einem Projekt per Kontextmenü auf eine Klasse/Methode ein UnitTest erstellen:

image

Aber erst mal zur Grundfrage:

Warum sollte ich Tests machen?

Jeder Entwickler will (hoffentlich) gute und funktionstüchtige Software schreiben, die möglichst fehlerfrei ihren Dienst tut.
In der Zeit wo die Software entwickelt wird, werden sicherlich an vielen Ecken (oder Software-Schichten) Änderungen eingepflegt oder die Applikation muss erweitert werden. Insbesondere in einem Team oder wenn eine größere Umstellung ansteht (Datenbasis wechselt, Logik muss abgeändert werden) wird es kritisch: Laufen alle Komponenten noch wie erhofft?
Je größer die Anwendung, desto größer wird der Aufwand der Betrieben muss, um sicherzustellen, dass alles noch läuft.
Ein Test im UserInterface ist zwar machbar, ist allerdings meist sehr anstrengend und zeit intensiv (sollte natürlich auch gemacht werden).
Es wäre doch viel schöner, wenn die Tests automatisch erfolgen könnten – ohne viel Zeit mit Klicken zu verlieren – auch das die Tests jederzeit ausgeführt werden können wäre doch nett, oder?
Hier kommen die UnitTests: Genau sowas machen UnitTests (und noch mehr ;) ).

Stellen wir uns mal vor…

… dass wir eine nicht ganz triviale Software haben, welche verschiedene Layer (Data/Business etc.) hat. Die Software funktioniert gut – der Kunde ist zufrieden und als Entwickler fühlt man sich wohl.
Wie es meistens ist: Der Kunde möchte eine Änderung. Ein neues Attribut soll hier und da angefügt werden, eine Abfragelogik verändert werden und die Validation der Daten soll anders verlaufen.
Das Problem: Die Änderungen können viele Bereiche betreffen, sodass es leicht passieren kann, dass plötzlich garnichts mehr geht. Aber wo genau hakt es denn? Erstmal überall den Debugger ansetzen und nachverfolgen… hoffentlich übersicht man kein Fehler.
Ergebnis: An dieser Stelle ist es meist für den Entwickler ein etwas mulmiges Gefühl – wird die Software noch genauso funktionieren wie vorher (natürlich mit den Änderungen)?

… nun mal mit Tests vorstellen (ein Gedankenspiel) :

Die verschiedenen Methoden wurden während der Entwicklung der Version 1 bereits mit UnitTests getestet. Daten eintragen, löschen, verändern, laden, validieren, Fehler abfangen usw. – alle Aspekte die wichtig sind, wurden als Test hinterlegt.
Nun kommen die Änderungen: Es werden einige kritische Bereiche verändert, aber nach jeder Veränderung kann man automatisch alle Tests abspielen – schlägt der Test fehl, weiß man, wo Handlungsbedarf besteht. Die eben gemachte Änderung war wohl anscheinend nicht so gut.
Nach einer ganzen Weile: Die Tests werden wieder bestanden – das Herz des Entwicklers schlägt höher. Es können zwar immer noch Fehler auftreten (vielleicht muss ein neuer Test für einen neuen Aspekt noch hinzugefügt werden), aber die Grundzüge der Applikation stimmen noch.

Klingt doch eigentlich gut, aber wie sieht das in der Praxis aus:

Ein sehr (zugegeben) doofes Beispiel:

    public class DataManager
    {
        public bool ConnectToData()
        {
            return true;
        }

        public List<int> GetData()
        {
            return new List<int>() { 1, 2, 3, 4, 5, 6, 7 };
        }
    }

Unser DataManager kann sich zu einer beliebigen Datenquelle verbinden – in unserem Fall sagen wir einfach mal, dass die Verbindung geklappt hat.
Die GetData Methode gibt Daten zurück – in unserem Beispiel ein paar statische Daten.

Da sich die Datenabfrage-Logik ja ändern könnte und da auch die Datenquelle vielleicht sich noch ändert, implementieren wir lieber einen Test dafür:

Create Unit Test…

image 
Methoden auswählen, welche man testen möchte (beide in unserem Fall)…

image 
Name eingeben…

image 

Ein TestProjekt ist entstanden:

image

Generierter Test (dort steht eigentlich bereits das wichtigste drin) :

Visual Studio nutzt MSTest – das Test-Framework von Microsoft. Es ist ähnlich zu nUnit und co.

using DoNot.Fear.UnitTests.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;

namespace DoNot.Fear.UnitTests.Test
{
    
    
    /// <summary>
    ///This is a test class for DataManagerTest and is intended
    ///to contain all DataManagerTest Unit Tests
    ///</summary>
    [TestClass()]
    public class DataManagerTest
    {


        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Additional test attributes
        // 
        //You can use the following additional attributes as you write your tests:
        //
        //Use ClassInitialize to run code before running the first test in the class
        //[ClassInitialize()]
        //public static void MyClassInitialize(TestContext testContext)
        //{
        //}
        //
        //Use ClassCleanup to run code after all tests in a class have run
        //[ClassCleanup()]
        //public static void MyClassCleanup()
        //{
        //}
        //
        //Use TestInitialize to run code before running each test
        //[TestInitialize()]
        //public void MyTestInitialize()
        //{
        //}
        //
        //Use TestCleanup to run code after each test has run
        //[TestCleanup()]
        //public void MyTestCleanup()
        //{
        //}
        //
        #endregion


        /// <summary>
        ///A test for GetData
        ///</summary>
        [TestMethod()]
        public void GetDataTest()
        {
            DataManager target = new DataManager(); // TODO: Initialize to an appropriate value
            List<int> expected = null; // TODO: Initialize to an appropriate value
            List<int> actual;
            actual = target.GetData();
            Assert.AreEqual(expected, actual);
            Assert.Inconclusive("Verify the correctness of this test method.");
        }

        /// <summary>
        ///A test for ConnectToData
        ///</summary>
        [TestMethod()]
        public void ConnectToDataTest()
        {
            DataManager target = new DataManager(); // TODO: Initialize to an appropriate value
            bool expected = false; // TODO: Initialize to an appropriate value
            bool actual;
            actual = target.ConnectToData();
            Assert.AreEqual(expected, actual);
            Assert.Inconclusive("Verify the correctness of this test method.");
        }
    }
}

Die Kommentare und auch den TestContext kann man löschen – ich hab ihn bisher nicht gebraucht ;)
Achtung: Ich bin kein Experte in den Unit Tests – sondern ist nur eine Art Erfahrungsbericht :)

Machen wir doch erstmal einen einfachen Test ob die Verbindung klappt:

        [TestMethod()]
        public void DataManager_ConnectToData_IsTrue()
        {
            DataManager man = new DataManager();
            Assert.IsTrue(man.ConnectToData());
        }

Sehr schlicht, aber genau das was ich wissen muss. Der Name des Tests sollte ungefähr das beschreiben was er macht – damit man sich später noch zurechtfindet. In diesem Fall prüfe ich einfach, ob die Verbindung zustande kommt.
Die Assert-Klasse hat mehrere Methoden:

image

Jetzt können wir diesen Test durchlaufen und sehen:

image

Jetzt prüfen wir noch die andere Methode:

        [TestMethod()]
        public void DataManager_GetData_IsNotNull()
        {
            DataManager man = new DataManager();
            Assert.IsNotNull(man.GetData());
        }

        [TestMethod()]
        public void DataManager_GetData_CheckForZero()
        {
            DataManager man = new DataManager();
            List<int> result = man.GetData();
            foreach (int number in result)
            {
                Assert.AreNotEqual(0, number);
            }
        }

Die erste Methode prüft, ob überhaupt Werte zurückkommen. Mit dem zweiten Test wollte ich nur mal eine primitive Business-Logik Test machen (“kein Element darf 0 sein”).

Jetzt kann man alle Abspielen:

image

Ergebnis:

image

Schön, oder? :)

Ein Gedankenspiel:

Angenommen in unseren Daten schleicht sich tatsächlich eine 0 ein (Datenabfrage könnte zum Beispiel falsch sein oder es wurden falsche Daten eingetragen), dann schauen wir mal was passiert:

image

Ergebnis:

image

Fail!

image

Idealerweise sollten Tests möglich häufig (sie können sogar automatisch nach jedem Build laufen!) machen – um die Fehlerquelle einzugrenzen.
Angenommen wir haben bei uns einen Fehler in der Abfragelogik oder die Methode (die bei uns nicht existiert, aber existieren könnte) die Daten schreibt, war fehlerhaft oder die Validation fehlgeschlagen ist… (es kann ja viele Quelle geben).

Wir beheben also diesen Fehler (den wir vielleicht sonst nur sehr schlecht gefunden hätten), bis wir wieder das sehen:

image

Resultat beim Entwickler (& beim zufriedenen Kunden) :

image

Der Testcode:

[TestMethod()]
        public void DataManager_ConnectToData_IsTrue()
        {
            DataManager man = new DataManager();
            Assert.IsTrue(man.ConnectToData());
        }

        [TestMethod()]
        public void DataManager_GetData_IsNotNull()
        {
            DataManager man = new DataManager();
            Assert.IsNotNull(man.GetData());
        }

        [TestMethod()]
        public void DataManager_GetData_CheckForZero()
        {
            DataManager man = new DataManager();
            List<int> result = man.GetData();
            foreach (int number in result)
            {
                Assert.AreNotEqual(0, number);
            }
        }


Ergebnis:

Die Vorteile von UnitTests werden sicherlich erst nach und nach bei einem Projekt sichtbar, aber wenn man dies stetig fortführt, reduziert sich die Fehleranfälligkeit erheblich.
Das was ich hier gezeigt habe, ist sicherlich nicht das Ende der Fahnenstange – es gibt neben Unit Tests auch noch andere Tests. Das ist auf der MSDN Testing Seite recht gut beschrieben.

Test Driven Development (TDD) :

TDD beschreibt ein Entwicklungsstil, wo auf Tests besonders viel Wert gelegt wird. Hier werden die Tests immer vor der eigentlichen Implementation geschrieben. Man trifft seine Annahmen und da die Methode (oder die zu testende Komponente) ja noch keine Logik enthält, wird der Test erst fehlschlagen.
Nun geht es darum, den Test erfolgreich zu bestehen. Sobald dies geschafft ist, kann man die Implementation hinterher nochmal überarbeiten. (Refactoring). Nun kann man immer wieder prüfen, ob der Test noch funktioniert oder nicht – wenn er nicht mehr stimmt, dann haben wir wohl was falsch gemacht.
Am Ende haben wir (in der Theorie) jede Methode / Komponente mit Tests ausgestattet.

Unit Tests in ASP.NET MVC, Silverlight & co.:

Eine Klassenbibliothek lässt sich relativ leicht testen. In ASP.NET (WebForms) ist dies allerdings nicht ganz so leicht. In ASP.NET MVC wurde darauf ein besonderer Augenmerk gelegt.
Auch in Silverlight 2 wurde das Thema angegangen.

Weitere Links:

Wer nun etwas neugierig geworden ist, der kann sich auch diese Links anschauen:

Download:

Sample Code

Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by

Hi, ich bin Robert Mühsig und bin Webentwickler und beschäftige mich mit Web-Frameworks auf dem Microsoft Web Stack und scheue mich auch nicht vor Javascript. Der Blog begann als "Problemsammelstelle und Lösungshilfe" und seitdem schreibe ich hier alles auf. Seit 2008 bin ich Microsoft MVP für ASP.NET. Treffen kann man mich online via Twitter (@robert0muehsig) oder hier.

11 Responses

  1. Sehr schöne Einführung zu Unittests. Hier erste Informationen zum ersten Release von Pex.

    http://dotnet-forum.de/blogs/rainerschuster/archive/2008/05/23/microsoft-research-mit-erstem-release-von-pex.aspx

    Reply
  2. Auch ich bedanke mich für diese Einführung!Die Thematik "Unit Tests" war mir auch immer ein Buch mit 7 Siegeln. Man liest zwar in der ganzen Entwicklergemeinde immer mal davon, doch man findet kaum einen geeigneten Einstieg in die Thematik. (Oder man scheut sich einfach davor … mit dem Vorurteil: "Viel zu kompliziert". Dabei ist ja das glatte Gegenteil der Fall!)Hinterher fragt man sich, warum man erst jetzt Unit Tests durchführt, obwohl es schon fast zu Spät dafür ist, wenn man schon mitten im Projekt ist und einem die Bugs erheblich Ärger machen. *g*Danke auch für den Link zu "nUnit"! Durch das minimalistische und konkrete Beispiel, welches dort präsentiert wurde, bin ich nun ganz gut dort durchgestiegen.nUnit wird wohl mein Standardtool für diesen Aspekt sein. (Einrichtung ist einfach; zudem ist es OpenSource und kann unabhängig von der Entwicklungsumgebung verwendet werden.)Speziell für die deutschsprachigen User, werde ich da wohl auch auf meinem Blog noch Tutorials diesbezüglich veröffentlichen. (Quasi eine Step-by-Step Anleitung)Nebenbei kann ich dabei mein eigenes Verständis prüfen.Vielleicht schreibe ich das Tutorial auch gleich bei Google-Knol. :-)mfgChristian

    Reply
  3. Geiles Teil !

    Reply
  4. Wirklich cool geschrieben, danke vielmals! :D

    Endlich mal nen Fuss in diese Testing Umgebung von Visual Studio rein gekriegt. Besonders der Tipp mit dem Rechtsklick zur Erstellung einer vorausgefüllten Testklasse fand ich sehr nützlich.

    Reply
  5. Danke für die Information. Endlich mal unterhaltsam geschrieben

    Reply
  6. geilo

    Reply

Comment on this post

Letzte Posts

  • image.png
    Azure AppInsights: Ein mini Google Analytics für Websites

    Seit der Build Konferenz ist das neue Azure Portal für alle freigeschaltet. Durch das neue Portal ist mir ein “Dienst” aufgefallen, den ich bisher nicht gesehen hatte: Azure Application Insights Viel weiss ich nicht über den Dienst, aber wer eine Azure Website hat der sollte mal die “Analytics” Box öffnen. Im Grunde handelt es sich […]

  • image.png
    Source Code veröffentlichen – aber bitte mit Lizenz

    Seit es den Blog gibt wird auch meist der gesamte Demo Source Code mit veröffentlicht. Das Ganze hatte ich am Anfang noch als .zip verteilt, später lag es mal auf Google Code und nun liegen alle Samples und sonstige Sachen auf GitHub. Beim letzten User Group Treffen in Zürich mit dem Titel “Open Source: Get […]

  • Fix: Cannot convert from ‘CConnectProxy::_ComMapClass *’ to ‘AddInDesignerObjects::IDTExtensibility2 *’

    Mal einen etwas esoterischer Blogpost, welcher auftaucht wenn man zu viel mit Office Addins rumspielt. Der Fehler passiert beim Bauen von C++ Projekten, welchen diesen Typ benötigen. Lösung (auf 64bit Systemen): C:\Program Files (x86)\Common Files\DESIGNER>regsvr32 MSADDNDR.DLL And Rebuild. Meine lieben Kollegen hatte mir dies schon mehrfach gesagt, allerdings hatte ich es immer wieder vergessen Das […]

  • Gegen das Gesetz verstoßen: X Jahre Haft. Gegen die Terms of Use verstoßen: Bann auf Lebenszeit. Danke Google & co.

    Bei fast allen Diensten die man im Internet nutzen kann muss man den “Terms of use” zustimmen. Völlig logisch dass da natürlich drin steht was erlaubt und was nicht. Wenn man gegen diese Regelungen verstößt hat das Unternehmen natürlich das Recht etwas dagegen zu unternehmen. In der heutigen Welt beherrschen einige wenige Unternehmen die digitale […]

  • image.png
    RSS Feed samt Kommentaranzahl und andere nicht Standard Elemente mit dem SyndicationFeed auslesen

    Jetzt mal ein Blogpost ohne ein fancy NuGet Package: Seit .NET 3.5 gibt es die SyndicationFeed Klasse. Eine schon etwas ältere API, reicht aber aus um Atom bzw. RSS Feeds zu lesen. In diversen RSS Feeds gibt es aber Erweiterungen, welche man natürlich auch auslesen möchte. So gibt WordPress z.B. auch die Anzahl der geposteten […]

Amazon Shop

Facebook