.
Anmeldung | Registrieren | Hilfe | Posteingang
Suchen
Home Foren News Member Offers Termine Developer Blogs Knowledge Base

Navigation

Skip Navigation Links.
Collapse Knowledge BaseKnowledge Base
Collapse TutorialsTutorials
Webentwicklung
Cliententwicklung
Datenbankentwicklung
IT Professional
Sharepoint
Collapse SprachspezifischSprachspezifisch
C#
Visual Basic
C++
XAML
SQL
JavaScript
Collapse ErfahrungsberichteErfahrungsberichte
Entwicklersoftware
Bücher
FAQ Grundlagen

Verknüpfungen

  • Knowledge Base durchsuchen
  • Hilfe zur Knowledge Base
  • RSS Feed
  • Twitter

Das kleine 1x1 des Test Driven Development (TDD Part 1)

Vorwort

Artikel zu diesem Thema gibt es wohl fast so viele wie Sand am Meer, ja ganze Bücher wurden schon darüber geschrieben. Warum also noch ein Artikel? Eine anschauliche Einführung in TDD scheint Mangelware zu sein. Dieser Text soll die Lücke füllen und Entwicklern, die bis jetzt noch keine praktische Berührung mit diesem Thema hatten, eine Einstiegshilfe sein.

Im Internet wird allerorten viel über das Für und Wider der testgetriebenen Entwicklung diskutiert. Dieser Artikel beteiligt sich nicht an dieser Diskussion. Er soll einen neutralen Blick auf das Thema werfen; aufzeigen was TDD überhaupt ist und wie man das Verfahren praktisch anwendet.

1. Test Driven Development, Unit Tests

1.1 Wozu überhaupt TDD?

Die Ziele der testgetriebenen Software-Entwicklung sind

  1. Qualitäts-Steigerung
  2. frühzeitige Erkennung von Bugs
  3. die Gewährleistung zur Einhaltung des SOC-Paradigmas (Separation Of Concerns)
  4. Hilfestellung für die Einhaltung bzw für das Erreichen von sauberem Code (CCD)
  5. Verbesserung der Testbarkeit

Wie man sieht, geht es gar nicht nur um das Aufspüren von Fehlern im Code! Das ist genau der Grund warum weltweit nach einem Begriff gesucht wird, der das Wort "Test" ersetzt und besser beschreibt, um was es hier eigentlich geht. Verabschieden sie sich von dem Gedanken dass es bei TDD nur um das Testen von Code bzw Funktionalität geht.

1.1.1 Wie wird die Qualität durch TDD verbessert?

Auch darüber gibt es jede Menge Artikel im Internet. Exemplarisch ein Link:

The three laws of TDD

1.1.2 Wie können Bugs frühzeitig erkannt werden?

Zum Einen passiert das natürlich schon nach dem zweiten Testlauf (der erste sollte rot werden, siehe Kapitel 1.2),
zum Anderen haben sie mit den Tests ein hervorragendes Diagnose-Instrumentarium. Nehmen sie irgendwo in ihrem Code Änderungen vor, machen sie anschließend einen Testlauf und sehen sofort ob alles funktioniert wie es soll.

1.1.3 Wie wird durch TDD das SOC-Paradigma gewährleistet?

Eine der Grundregeln für Unit Tests ist, dass sie atomar sein müssen. Das bedeutet, der zu testende Code sollte genau eine Funktion abdecken. Diese eine Funktion soll so gestaltet sein dass sie nicht wiederum andere Funktionen aufrufen muss.

1.1.4 Wie erhält man durch TDD sauberen Code?

Na gut, auch Test Driven Development kann aus Dreck keine Sahne zaubern. In diesem Punkt ist TDD eine Erziehungsmaßnahme für den Entwickler. Vielleicht ist das auch einer der Gründe warum einige sich dagegen wehren. Eines steht fest: möchte  man sauberen Code schreiben, hilft TDD dabei. Christoph Tohermes, ein Kollege aus der .Net-Entwicklergemeinde, hat dazu einen schönen Artikel geschrieben: TDD als Erziehungsmaßnahme

1.1.5 Wie wird die Testbarkeit verbessert?

Zugegeben, eine sehr banale Frage. Wäre der Code nicht testbar, könnten keine Unit Tests dafür geschrieben werden. Ganz richtig, aber wäre ihr Code vorher auch so testbar gewesen? Durch den konsequenten Einsatz von testgetriebener Entwicklung erreichen sie von Beginn an gute Testbarkeit ihres Codes.

1.1.6 Wie funktioniert TDD?

Das “verrückte” an test driven Development ist das “test first” Gesetz. Ja, bevor überhaupt Produktivcode entsteht, werden Unit Tests geschrieben! Dabei kommt “red-green-refactor” zum Einsatz. Was man darunter versteht und wie das funktioniert, erläutere ich im nächsten Kapitel.
Übrigens benutze ich hier das in Visual Studio implementierte MSTest Testframework. Das soll keine Wertung sein.

 

1.2 Eine praktische Demonstration von Test Driven Development

Wegen einer kreativen Flaute (Wetter?) fiel mir erst kein konkretes Projektbeispiel ein, das für diesen Artikel herhalten könnte. Dann bin ich auf das SpecificationPattern bei Wikipedia gestoßen. Der Autor hat nur ein rudimentäres Anwendungsbeispiel hinterlassen, aber dafür Anforderungen in Textform. Eigentlich steigt mein Blutdruck jedes mal wenn ich ein Sample sehe das nicht komplett ist. Dieses mal aber ist es gut. Das ist genau die Situation, der man als Entwickler wohl am Anfang eines Projekts gegenüberstehen kann. Ein Kunde definiert Anforderungen, der oder die Entwickler erarbeiten ein Domänenmodell.
Klassen und Methoden müssen deklariert und mit Leben gefüllt werden. Das ist geeignet für Unit Tests.
Ein Service wird auch angesprochen, also müssen Mocks verwendet werden. Und so ganz nebenbei lernen sie auch noch das Specification Pattern kennen.
Fangen wir an.

Phase 1 (Planung)

Zuerst einmal verschaffen sie sich einen Überblick. Alle Objekte die das eigentliche Pattern bilden, sind komplett. Darum müssen sie sich also nicht zu kümmern.
Als nächstes schauen sie sich das Anwendungsbeispiel an:

using System;

using System.Collections.Generic;

 

namespace SpecificationPatternWikipedia

{

   internal class Program

   {

      private static void Main()

      {

         var overDue = new OverDueSpecification();

         var noticeSent = new NoticeSentSpecification();

         var inCollection = new InCollectionSpecification();

 

         ISpecification<Invoice> sendToCollection = overDue.And(noticeSent).And(inCollection.Not());

 

         IEnumerable<Invoice> invoiceCollection = Service.GetInvoices();

 

         foreach(Invoice currentInvoice in invoiceCollection)

         {

            if(sendToCollection.IsSatisfiedBy(currentInvoice))

            {

               currentInvoice.SendToCollection();

            }

         }

      }

   }

}

Aha, jede Menge Objekte die noch gar nicht existieren (in rot dargestellt). Das Code Completion Feature in Visual Studio (oder auch Resharper) hilft uns dabei, diese Objekte zu erzeugen. Wenn es ganz "test driven" sein soll, könnten diese Elemente auch bei der Gestaltung der Unit Tests erzeugt werden.

Code Completion

Phase 2 (Planung forts.)

Nachdem das bei allen fehlenden Objekten gemacht wurde, präsentiert sich der Code so:

   internal class Program

   {

      private static void Main()

      {

         var overDue = new OverDueSpecification();

         var noticeSent = new NoticeSentSpecification();

         var inCollection = new InCollectionSpecification();

 

         ISpecification<Invoice> sendToCollection = overDue.And(noticeSent).And(inCollection.Not());

 

         IEnumerable<Invoice> invoiceCollection = Service.GetInvoices();

 

         foreach(Invoice currentInvoice in invoiceCollection)

         {

            if(sendToCollection.IsSatisfiedBy(currentInvoice))

            {

               currentInvoice.SendToCollection();

            }

         }

      }

   }

 

   internal class OverDueSpecification

   {

   }

 

   internal class NoticeSentSpecification

   {

   }

 

   internal class InCollectionSpecification

   {

   }

 

   internal class Invoice

   {

      public void SendToCollection()

      {

         throw new NotImplementedException();

      }

   }

 

   internal static class Service

   {

      public static IEnumerable<Invoice> GetInvoices()

      {

         throw new NotImplementedException();

      }

   }

Phase 3 (Planung Forts.)

Es gibt immer noch zwei "rote" Objekte, And und Not. Das liegt daran dass die Specification-Klassen das ISpecification Interface noch nicht implementieren. Nachdem das gemacht wurde, sieht es so aus:

   internal class Program

   {

      private static void Main()

      {

         var overDue = new OverDueSpecification();

         var noticeSent = new NoticeSentSpecification();

         var inCollection = new InCollectionSpecification();

 

         ISpecification<Invoice> sendToCollection = overDue.And(noticeSent).And(inCollection.Not());

 

         IEnumerable<Invoice> invoiceCollection = Service.GetInvoices();

 

         foreach(Invoice currentInvoice in invoiceCollection)

         {

            if(sendToCollection.IsSatisfiedBy(currentInvoice))

            {

               currentInvoice.SendToCollection();

            }

         }

      }

   }

 

   //======================================================================

 

   internal class OverDueSpecification : ISpecification<Invoice>

   {

      public bool IsSatisfiedBy(Invoice entity)

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   internal class NoticeSentSpecification : ISpecification<Invoice>

   {

      public bool IsSatisfiedBy(Invoice entity)

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   internal class InCollectionSpecification : ISpecification<Invoice>

   {

      public bool IsSatisfiedBy(Invoice entity)

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   internal class Invoice

   {

      public void SendToCollection()

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   internal static class Service

   {

      public static IEnumerable<Invoice> GetInvoices()

      {

         throw new NotImplementedException();

      }

   }

Für die bessere Übersicht habe ich Trennlinien zwischen die Klassen gesetzt. Normalerweise würde jede Klasse in eine eigene Datei gepackt. Hier wurde für eine bessere Präsentation darauf verzichtet.

Anmerkung: In der Praxis werden sie wohl kaum jemals den Anwendungscode von ihrem Kunden vorgesetzt bekommen, den sie dann nur noch mit Leben füllen müssen, so wie es hier gemacht wurde. Was bis hierhin geschehen ist, würde in der Praxis der Planungsphase entsprechen, die auch bei TDD zuerst kommt.

Phase 4 (Red)

Weiter: Wir sehen fünf NotImplementedExceptions. Eigentlich würde jetzt der Startschuss für die Unit Tests fallen, in denen diese Methoden, so wie sie sind, getestet würden. Jeder weiß dass die Tests rot würden. Ich denke, eine Demonstration kann ich mir sparen.

Phase 5 (Green)

Laut Lehrbuch sollten nun Stubs in diese Methoden gebaut werden. Das heißt zum Beispiel: haben sie eine Methode mit einem bool-Return, würde einfach return true; hineingeschrieben und in dem entsprechenden Test mittels Assert.IsTrue(MyBooleanMethod(...)); festgestellt ob die Testmethode OK ist, denn dieser Test sollte grün werden. Hier ein Beispiel:

       internal class InCollectionSpecification : ISpecification<Invoice>

       {

          #region ISpecification<Invoice> Members

    

          /// <summary>

          /// Is satisfied when an invoice has already been sent to the collection agency.

          /// </summary>

          public bool IsSatisfiedBy(Invoice entity)

          {         

             return true;

          }

    

          #endregion ISpecification<Invoice> Members

       }

Und dazu der Unit Test:

      [TestMethod]

      public void InvoiceShouldAlreadyBeSentToCollectionAgency()

      {

         var inCollectionSpecification = new InCollectionSpecification();

         Assert.IsTrue(inCollectionSpecification.IsSatisfiedBy(new Invoice()));

      }

Wie man sieht, übergebe ich ein leeres Invoice-Objekt, denn das spielt in dieser Phase keine Rolle; in der nächsten Phase aber sehr wohl und der Test müsste damit vor die Wand laufen! Das ist so geplant, aber dazu mehr wenn es an der Zeit ist.

An dieser Stelle möchte ich kurz einhaken. Solche Stubs sind bei mir lange Zeit auf Unverständnis gestoßen. Als TDD-Anfänger fragt man sich unweigerlich, "warum soll ich testen ob eine Methode true zurück gibt, wenn sie ganz offensichtlich gar nichts anderes kann als das?". Denken sie das auch? Hier wird aber gar nicht die Methode IsSatisfiedBy getestet, sondern der Unit Test testet sich selber! Warum? Bevor sie zur nächsten Phase übergehen, müssen sie ganz sicher sein dass sie (zum Beispiel) auch wirklich die richtige Komponente referenzieren, oder nicht irgend einen dummen Tippfehler gemacht haben, der  zufällig compiliert, aber wenn unentdeckt, später üble Folgen haben kann.

Phase 6 (Refactor)

Sie kommen nun zum ersten Refactoring und sie machen sich Gedanken wie diese Methoden belebt werden. Also, was soll dort passieren? Jetzt kommen die Anforderungen wieder ins Spiel. Ich kopiere nun die jeweiligen Anforderungen als Kommentar an diese Objekte und definiere erste Funktionalität.

   internal class Program

   {

      private static void Main()

      {

         var overDue = new OverDueSpecification();

         var noticeSent = new NoticeSentSpecification();

         var inCollection = new InCollectionSpecification();

 

         ISpecification<Invoice> sendToCollection = overDue.And(noticeSent).And(inCollection.Not());

 

         IEnumerable<Invoice> invoiceCollection = Service.GetInvoices();

 

         foreach(Invoice currentInvoice in invoiceCollection)

         {

            if(sendToCollection.IsSatisfiedBy(currentInvoice))

            {

               currentInvoice.SendToCollection();

            }

         }

      }

   }

 

   //======================================================================

 

   internal class OverDueSpecification : ISpecification<Invoice>

   {

      #region ISpecification<Invoice> Members

 

      /// <summary>

      /// Is satisfied when the due date is 30 days or older.

      /// </summary>

      public bool IsSatisfiedBy(Invoice entity)

      {

         return DateTime.Now - entity.DueDate > new TimeSpan(30, 0, 0, 0);

      }

 

      #endregion ISpecification<Invoice> Members

   }

 

   //======================================================================

 

   internal class NoticeSentSpecification : ISpecification<Invoice>

   {

      #region ISpecification<Invoice> Members

 

      /// <summary>

      /// Is satisfied when three notices have been sent to the customer.

      /// </summary>

      public bool IsSatisfiedBy(Invoice entity)

      {

         return entity.Customer.AmountOfNoticesSent >= 3;

      }

 

      #endregion ISpecification<Invoice> Members

   }

 

   //======================================================================

 

   internal class InCollectionSpecification : ISpecification<Invoice>

   {

      #region ISpecification<Invoice> Members

 

      /// <summary>

      /// Is satisfied when an invoice has already been sent to the collection agency.

      /// </summary>

      public bool IsSatisfiedBy(Invoice entity)

      {

         /* Question: Soll der Customer ein Property erhalten,

          * das angibt ob die collection agency benachrichtigt wurde,

          * oder soll ich ein CollectionAgency-Objekt machen,

          * oder eine Serviceverbindung mit einem Request?

          * Ich entscheide mich für einen Service-Request.*/

 

          throw new NotImplementedException();

      }

 

      #endregion ISpecification<Invoice> Members

   }

 

   //======================================================================

 

   /// <summary>

   /// Connects to the business database.

   /// </summary>

   internal static class Service

   {

      public static IEnumerable<Invoice> GetInvoices()

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   public class Invoice

   {

      public string InvoiceNumber { get; set; }

 

      public Customer @Customer { get; set; }

 

      public DateTime DueDate { get; set; }

 

      public void SendToCollection()

      {

         throw new NotImplementedException();

      }

   }

 

   //======================================================================

 

   public class Customer

   {

      public string Name { get; set; }

 

      public int AmountOfNoticesSent { get; set; }

 

      // more properties

   }

Durch die Implementierung von Funktionalität sind ganz zwangsläufig und nebenbei ein paar neue Objekte entstanden. So zum Beispiel die Customer-Klasse mit dem AmountOfNoticesSent-Property, oder das DueDate-Property der Invoice-Klasse.

Die erste Anforderung lautet, "Is satisfied when the due date is 30 days or older". Die Rechnung ist also überfällig wenn das Fälligkeitsdatum 30 Tage oder mehr zurückliegt. Ich muss hier mit Datumswerten arbeiten. Oh oh, das habe ich noch nie gemacht! Na ist doch prima, auch für Experimente sind Unit Tests perfekt geeignet. Ich probiere einfach mal

return DateTime.Now - entity.DueDate > new TimeSpan(30, 0, 0, 0);

und schreibe zwei Unit Tests dafür.

      [TestMethod]

      public void ShouldBeOverDue()

      {

         // Arrange

         var overDueSpecification = new OverDueSpecification();

         var invoice = new Invoice

                          {

                             Customer = new Customer { Name = "ACME" },

                             DueDate = new DateTime(2010, 6, 15),

                             InvoiceNumber = "1"

                          };

         // Act

         bool result = overDueSpecification.IsSatisfiedBy(invoice);

         // Assert

         Assert.IsTrue(result);

      }

 

      [TestMethod]

      public void ShouldNotBeOverDue()

      {

         // Arrange

         var overDueSpecification = new OverDueSpecification();

         var invoice = new Invoice

                          {

                             Customer = new Customer { Name = "ACME" },

                             DueDate = DateTime.Now,

                             InvoiceNumber = "1"

                          };

         // Act

         bool result = overDueSpecification.IsSatisfiedBy(invoice);

         // Assert

         Assert.IsFalse(result);

      }

Der Testlauf wird grün - das hat ja super geklappt!
Die weiteren Unit Tests sind schnell geschrieben.

      [TestMethod]

      public void ShouldNotBeSatisfiedBecauseLessThan3NoticesSent()

      {

         var noticeSentSpecification = new NoticeSentSpecification();

         var invoice = new Invoice

         {

            Customer = new Customer { Name = "ACME", AmountOfNoticesSent = 0 }

         };

         bool result = noticeSentSpecification.IsSatisfiedBy(invoice);

         Assert.IsFalse(result); 

      }

 

      [TestMethod]

      public void ShouldBeSatisfiedBecause3NoticesSent()

      {

         var noticeSentSpecification = new NoticeSentSpecification();

         var invoice = new Invoice

         {

            Customer = new Customer { Name = "ACME", AmountOfNoticesSent = 3 }

         };

         bool result = noticeSentSpecification.IsSatisfiedBy(invoice);

         Assert.IsTrue(result); 

      }

Was jetzt noch bleibt, sind Tests die man nicht einfach auf den Produktivcode loslassen darf!
Das sind im Einzelnen, Tests für die

  • GetInvoices-Methode der Service-Klasse,
  • SendToCollection-Methode der Invoice-Klasse,
  • InCollectionSpecification.IsSatisfiedBy Methode,
  • GetInvoice-Methode der ICollectionAgencyService-Implementation.

Vorschau

Warum hier spezielle Testverfahren nötig sind und wie diese geschrieben werden, erkläre ich im nächsten Artikel über Mocking.
Bis dahin wünsche ich viel Spaß und Erfolg auf dem neuen Terrain der testgetriebenen Software-Entwicklung.

Rainer Hilmer

von Rainer Hilmer, 30.07.2010 zugeordnet zu IT Professional , Tutorials .

Kommentare

Es sind noch keine Kommentare vorhanden.

Eigener Kommentar

Sie müssen angemeldet sein, um ein Kommentar zu erstellen.
  • Schwierigkeit: Profis
  • Views: 3525
  • Zur Druckversion
  • Artikel von Rainer Hilmer

Kick it on dotnet-kicks.de

Artikel

Autor

Kick it!

Wenn ihnen dieser Artikel gefällt, bitte "kicken" sie ihn.

WPF Forum | ASP.NET Forum | ASP.NET MVC Forum | Silverlight Forum | Windows Phone 7 Forum | SharePoint Forum | Dotnet Jobs | Dotnet Termine | Developer Blogs | Dotnet News

Das Team | Regeln | Impressum