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.

  • http://dotnet-forum.de/blogs/rainerschuster/ Rainer Schuster

    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

  • Pingback: HowTo: 3-Tier / 3-Schichten Architektur | Code-Inside Blog

  • Pingback: HowTo: Eigene .NET Events definieren und mit Unit-Tests testen | Code-Inside Blog

  • Pingback: HowTo: UnitTests und Einführung in Mocking mit Rhino.Mocks | Code-Inside Blog

  • http://chinafansblog.blogspot.com/ Christian Götz (ChinaFan)

    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

  • Pingback: HowTo: TeamCity & MSTests | Code-Inside Blog

  • Pingback: HowToCode: Braucht man UML? | Code-Inside Blog

  • http://kastor.wordpress.com Kastor

    Geiles Teil !

  • Efrain

    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.

  • http://Tollgeschrieben Ukirsche

    Danke für die Information. Endlich mal unterhaltsam geschrieben

  • http://Website Eder

    geilo

Letzte Posts

  • image_thumb.png
    NuGet Package Restore & Build Server wie z.B. AppVeyor

    NuGet ist ja mittlerweile weit verbreitet, aber eine Frage stellt sich natürlich immer noch: Checkt man die NuGet Packages ein oder nicht? In meinem kleinen Side-Projekt, welches auf GitHub liegt und ich über AppVeyor auch bauen lasse nutze ich das Package Restore Feature von NuGet, d.h. in meinem Repository befindet sich kein NuGet Package mehr, […]

  • image.png
    Microsoft Account Login via ASP.NET Identity

    Der Microsoft Account ist die zentrale Identifikationsstelle in der “Consumer-Microsoft-Welt”, allerdings ist das Einbinden eben dieser in die eigene Applikation eher schwierig. Das “Live SDK” ist nun unter dem OneDrive Dev Center zu finden und ganz professionell wurden auch alle Links zum alten Live SDK damit unbrauchbar gemacht. Beim Microsoft Account ist es auch unmöglich […]

  • image.png
    Zeitgesteuerte Azure WebJobs – so einfach kann Azure sein

    Das noch in Entwicklung befindliche Azure WebJob SDK bietet einige coole Features zum Verarbeiten und Bereitstellen von Daten. Bekanntes Beispiel ist das Sample welches auf eine Azure Queue lauscht und sobald ein Item da vorhanden ist anfängt dies zu verarbeiten. Szenario: Zeitgesteuerte Aktivitäten – ohne Queue und co. Mein Szenario war allerdings wesentlich trivialer: Ich […]

  • image.png
    Get Involved in OSS! Ja, aber wie geht das denn mit GitHub?

    Auch im .NET Lager gibt es Bewegung im OSS Bereich und es gibt verschiedene Arten wie man bei einem Open Source Projekt “Contributed”. Was zählt alles zu “Contribution”? Unter “Contribution” läuft eigentlich alles – ob es Fragen/Probleme zu dem Projekt via Issues ist oder Dokumentation nachreicht oder ob man darüber bloggt oder das Projekt vorstellt. […]

  • HowTo: Web.config samt eigener ConfigSection zur Laufzeit ändern

    In dem HowTo geht es darum wie man die Web.config zur Laufzeit ändert und was es dabei zu beachten gilt. Das ganze klappt auch mit komplexeren ConfigSections. Eigene ConfigSection? Vor einer ganzen Weile habe ich mal über das Erstellen einer eigenen ConfigSection geschrieben – im Grunde nutzen wir jetzt fast dieselbe Config. Zur Laufzeit? Startet […]

Amazon Shop

Facebook