Coding Dojo – der Trainingsraum für Entwickler

Definition: A Coding Dojo is a meeting where a bunch of coders get together to work on a programming challenge (the Code Kata). They are there to have fun and to engage in deliberate practice in order to improve their skills.

(Quelle: http://codingdojo.org/)

The Principles of a Coding Dojo
(extracted from the Laurent Bossavit’s Blog: bossavit.com/dojo/archives/2005_02.html)

  1. The First Rule
    One important rule about the Dojo is : At the Dojo one can’t discuss a form without code, and one can’t show code without tests. It is a design training place, where it is acknowledged that “the code is the design” and that code without tests simply doesn’t exist.
  2. Finding a Master
    The master can’t be a master of every forms. I feel quite at ease with recursive functions and list processing e.g. but I think I don’t know how to create even a simple web app. Fortunately, while it’s the first time they really deal with “tail-recursion” some practionners here have done professional web apps for years!
  3. Come Without Your Relics
    Of course, you know how to do it. You know how and why this code is better than that one. You’ve done it already. The point is to do it right now, explain it to us, and share what you learned.
  4. Learning Again
    In order to learn again something, we just have to forget it. But it’s not easy to forget something when you’re alone. It’s easier when we give our full attention to someone who just tries to learn it for the first time. We can learn from others mistakes as well as from ours if we listen carefully.
  5. Slow Down
    Learning something should force you to slow down. You can go faster because you learned some tricks, but you cannot go faster and learn at the same time. It’s OK, we’re not in a hurry. We could do that for years. Some of us certainly will. What kind of deadline will we miss if we spend four more weeks on this subject rather than on four different subjects? More precisely, when we reach the next plateau, is it because we went through the previous one, or is it just because we were flying over it?
  6. Throwing Yourself In
    At some time someone begins to master a subject and wants to approach another one. Those threatened by boredom should throw themselves first into a presentation. The goal is to get back to a good motivation level, ie. an acceptable level of difficulty.
  7. Subjecting To A Master
    If it seems difficult to you, look for other practitionners who can judge your code and could easily show something new about it to you. Ask again until the matter contains absolutely no more difficulty to you.
  8. Mastering A Subject
    If it seems easy to you, to explain it to other who find it difficult. Explain it again as long as they find it difficult.

(Quelle: http://codingdojo.org/)

Seit mehreren Monaten experimentieren wir mit Coding Dojos in der Firma. Nach jedem bisherigen Dojo haben wir etwas gelernt und diese Erkentnisse bei dem nächsten Session gleich angewendet: Wir haben sozusagen die Dojo-Finding-Kata gelöst und eine gute Lösung dafür gefunden, wie wir dabei das meiste lernen können.

Es gibt 2 Levels: für Anfänger und für weniger Anfänger, mit einem gemeinsamem Punkt: alle sind Softwareentwickler. Anfänger bedeutet: neu in der Welt der Tests und/oder neu in C#. Dadurch, dass wir sowohl C# als auch JavaScript-Dojos machen, werden wir uns mal den einen mal den anderen Schuch anziehen müssen ;). Durch diese Aufteilung sind wir in der Lage, eine passende Übung durchzuführen. Die Einladung geht an alle und es bleibt jedem selbst überlassen, zu entscheiden, bei welchem Level er/sie mitmachen möchte. Diese Selbst-Verpflichtung führt automatisch dazu, dass man es ernst.

Was die Form betrifft, haben wir mehrere Varianten ausprobiert:

  • Mit oder ohne Moderator: Erkenntnis: es muss einen Moderator geben, wenn man nicht will, dass der Abend in Frust und Streit endet 😉
  • mit Coder und Driver (Randori Modus): Erkenntnis: der Coder war gleichzeitig der Driver, der andere saß nur da und wartete auf seine ReiheThumbs-down-icon
  • mit Wechsel der Paare nach einer bestimmten Zeit: Erkenntnis: das mentale Ticken einer Uhr hat teilweise zu einer kompletten Blockade geführt. Warum soll man überhaupt einen Stopper verwenden? Niemand sagt einem bei der Arbeit: du hast 5 Minuten für diese Aufgabe sonst müssen wir die Firma schließen!!Thumbs-down-icon
  • ein Dojo für alle, unabhängig von Kenntnissstand: Erkenntnis: ein guter Entwickler, der in C# noch noch nicht so bewandert ist, hat genauso gute oder gar bessere Ideen, als die anderen bzw. braucht länger um den Syntax des Lösungsweges zu verstehen. Das kann dazu führen, dass man sich unterschätztThumbs-down-icon

Nach verschiedenen Diskussionen mit anderen Entwickler, die Dojos in Unternehmen erfolgreich etabliert haben (danke nochmal Ralf und Uli) und nach diesen eigenen Erfahrungen, haben wir uns für die folgende Variante entschieden:

  • die Tastatur geht herum, und nicht der Entwickler (das hat auch immer wieder zu Störungen geführt).
  • Jeder, der dran ist, muss entweder den allerersten Test schreiben oder den vorherigen lösen: das bedeutet, er muss den Code schreiben, wodurch der Test grün wird – nicht mehr und nicht weniger. Danach muss er noch den nächsten Test schreiben, und zwar so, dass er rot ist. (Es gab Fälle, wo die nächste Anforderung – also der nächste Test – gleich mit implementiert wurde.)
  • Es gibt keine zeitliche Begrenzung und niemand muss alleine grübeln. Wenn Fragen oder Mißverständnisse im Raum stehen, sie werden sofort geklärt. Die Fragen müssen trotzdem vom Entwickler formuliert werden: der Lernprozess ist immer da.
  • Der Weg ist das Ziel: wird eine Aufgabe nicht in der Zeit erfüllt, die uns zur Verfügung steht, dann ist das auch ein Erkenntniss, aus dem man lernen kann.

Thumbs-up-icon

Wir verfolgen durch diese Trainingsstunden verschiedene Ziele:
– lernen, wie man Tests schreibt, um eine nachhaltige Qualität abzusichern,
– lernen, wie man Features gegen Code-Veränderungen schützt.
– lernen, wie man eine Anforderung interpretiert,
– lernen, wie man eine Anforderung definiert,
– lernen, wie man eine konstruktive Diskussion führt um sich auf eine Lösung zu einigen,
– lernen, wie man SOLIDen Code schreibt, Abhängigkeiten erkennt und trennt, wie man für Menschen lesbaren Code schreibt.

Die Liste der Lerneffekten solcher Übungen ist sicher viel länger, aber vorerst reicht es, wenn wir das hier schaffen :). Diese Prozesse werden sich sicherlich ändern – genauso wie wir Entwickler. Die Grundlagen aber bleiben bestehen: alles, was wir hier lernen, soll sich – wird sich –  in der täglichen Arbeit wiederspiegeln und uns zu besseren Softwareentwickler und gleichzeitig zu besseren Teamplayer machen.

Sind Unit Tests wirtschaftlich untragbar?

Immer wieder höre ich die Aussagen “Unit Tests sind schön und gut, wir haben nur keine Zeit dafür.” Oder “Klar, man kann Tests machen, hauptsache, es nimmt nicht zu viel Zeit von der Arbeit weg” ??!!

 

Nur um Missverständnisse zu vermeiden: Tests macht man nicht zum Spaß oder aus Langeweile, die Tests stellen die Essenz, die abstraktester Form der Lösung dar!

 

Ich habe bis heute Schwierigkeiten damit, meinem Gegenüber zu erklären, dass er sich irrt. Ich WEIß es einfach aus Erfahrung, dass dies eine Milchmädchenrechnung ist. Keine ernsthafte Argumente gegen Tests würden bei einer tieferen Überprüfung standhalten. Die Pros übertreffen klar die Kontras. Aber wie soll ich etwas – für mich – Offensichtliches in Worte fassen? Wie soll ich etwas in ein paar Sätzen erklären, was ich in einem andauernden Prozess durch jeden NICHT (oder nicht richtig) geschriebenen Test gelernt habe? Oder durch jeden Aha-Effekt oder durch jeden stressfreien Release (kein Stress entsteht, wo kein Platz für Bugs existiert 😉 )

 

Unit Tests sind für den Open Mind “selbsterklärend”: wenn der Bug in einem ungetesteten Code steckt, dann wird das zu einer “blinden” Fehlersuche führen, die Tage dauern kann und auf jedem Fall Geld und Ruf kostet. Wie lange dauert es, den Fehler in einem getesteten Codebasis zu finden, wo die Eingrenzung innerhalb von Sekunden erfolgt? Wie oft kommt es überhaupt vor, dass dieses Problem entsteht? Für mich schaut die Rechnung so aus:

 
Zeit_für_fehlersuche = Unproduktive_Zeit;
f(Unproduktive_Zeit) = Verschwendetes_Geld;
 
0->Zeit_für_fehlersuche(getesteter_Code)----------------------->Zeit_für_fehlersuche(ungetesteter_Code)---.....oo
 

Ok, ich glaube, ihr kennt jetzt meinen Standpunkt 😉 Aber ich bin ja nicht die ultimative Maßstab dafür, wie man arbeiten sollte. Deshalb habe ich ein paar Artikel und Statistiken von klügeren Leuten zusammengesucht, bitte liest die auch.

 

Diese Infos habe ich bei stackoverflow gefunden:

Realizing quality improvement through test driven development: results and experiences of four industrial teams und hier eine Diskussion darüber.

The study and its results were published in a paper entitled Realizing quality improvement through test driven development: results and experiences of four industrial teams, by Nagappan and research colleagues E. Michael Maximilien of the IBM Almaden Research Center; Thirumalesh Bhat, principal software-development lead at Microsoft; and Laurie Williams of North Carolina State University. What the research team found was that the TDD teams produced code that was 60 to 90 percent better in terms of defect density than non-TDD teams. They also discovered that TDD teams took longer to complete their projects—15 to 35 percent longer.

“Over a development cycle of 12 months, 35 percent is another four months, which is huge,” Nagappan says. “However, the tradeoff is that you reduce post-release maintenance costs significantly, since code quality is so much better. Again, these are decisions that managers have to make—where should they take the hit? But now, they actually have quantified data for making those decisions.”

Es gab auch kleinere Experimente dazu wie z.B. Code Lab – TDD vs. Non-TDD

Over 3 iterations, average time taken to complete the kata without TDD was 28m 40s. Average time with TDD was 25m 27s. Without TDD, on average I made 5.7 passes (delivering into acceptance testing). With TDD, on average I made 1.3 passes (in two attempts, they passed first time, in one it took 2 passes.)

Now, this was a baby experiment, of course. And not exactly laboratory conditions. But I note a couple of interesting things, all the same.

Und weil ein Bild mehr als tausend Worte spricht: Die Kostenverteilung bei getesteten Code schaut ungefähr so aus
Testing Benefits

Tests schreiben ist einfach, der Ertrag ist riesig. Warum soll man also keine Tests schreiben? Sind wir wirklich unfehlbar, schreiben wir immer den perfekten Code? Seien wir mal ehrlich…Ich bin es sicher nicht und ihr auch nicht.

 

Und hier noch die obligatorische Buchempfehlung: The Art Of Unit Testing Das Buch ist wunderbar verständlich geschrieben mit echten Beispielen und guten Argumenten, warum und wie man testen soll.

Tausende Codezeilen verstehen – aber wie?

Nach 7 Wochen Zwangsurlaub (siehe unten) bin ich wieder back to life: mit einem halbwegs neuen Bein (mit Titaneinlagen und Schlitzschrauben 😉 ), in einer neuen Stadt mit 100%-er Snowcoverage, in einem neuen Job – nach einem Monat Verspätung.

Das letzte Mal, als ich bei einer Firma den Code verstehen musste, ging es um ASP-Classic. Die Abhängigkeiten waren überschaubar, das Debugen ging mit Response.Write-Zeilen ;). Aber wie macht man das heute, wie versteht man den bestehende Code, der in vielen Jahren aus den fleißigen Fingern der Programmierern herausgeflossen ist? Und das im Web, in einer unglaublich flexiblen E-Commerce-Anwendung…

Die Lösung war einfach: mit Unit-Tests ! Ich musste nicht erklärt bekommen haben, WAS der Code tut, nur welche Aufrufe zu welchen Ergebnissen führen. Immer, wenn ich ein Szenario verstanden habe, wurden dafür Tests geschrieben und das Ergebnis besprochen. Der Begriff unit wurde teilweise natürlich ausgedehnt, aber das hat nichts an der Tatsachen geändert, dass am Ende

  1. ich den Code verstanden habe
  2. die meisten analysierten Zeilen durch einen Test abgedeckt wurden
  3. die Diskussionen über den Testnamen dazugeführt haben, dass manch unnötige Zeilen (lese “Szenarien”) entfernt wurden, also der Code besser geworden ist

Webforms mit TDD entwickeln

Diese hier ist schon wieder eine wunderbare Idee aus Jimmy Nilssons Applying Domain-Driven Design and Patterns (das Buch scheint bis zur letzten Seite super Tipps zu liefern 😉 ) und zwar von Ingemar Lundberg.

Was ist eigentlich die Aufgabe einer Webseite: irgendwelche Controls mit Text zu füllen. Was dieser Text beinhaltet, dass wird von verschiedenen Funktionen entschieden. (Wie er ausgegeben wird, interessiert nicht.) Die Hauptaufgabe also bei der testgetriebenen Entwicklung von Webforms ist, diese Funktionalitäten zu ermitteln und zu implementieren. So bekommt man eine Webanwendung, bei der die Hauptbereiche getestet sind und nur die eigentliche Html-Ausgabe ungetestet bleibt. Außerdem wird auf dieser Art sichergestellt, dass die View sonst nichts tut.

Nehmen wir ein einfaches Beispiel: das Füllen eines Warenkorbs. Es stehen 3 Produkte zur Auswahl und der Käufer darf in seinen Warenkorb maximal 3 stellen. Um etwas Logik dabei zu haben, wird festgelegt, dass von ein Produkt nur maximal 2-mal gewählt werden darf. Wenn diese Bedingung erfüllt ist, soll das Produkt nicht mehr auswählbar sein. Gleiches gilt, wenn im Korb bereits 3 Produkte sind, kein Produkt darf mehr auswählbar sein.

Shopping Cart

Das Auswählen eines Produktes passiert z.B. mit einem OnClick-Event auf dem Link. Aus der Sicht der Funktionalität ist das nicht wichtig, hauptsache das Event wird ausgelöst.

Was tut also ein Modell um eine View zu steuern: nachdem es sichergestellt hat, dass alle Controls leer sind, lädt es die Daten mit irgendeiner Repository (nennen wir sie IDeposit), gibt sie der View und veranlasst diese, die Daten zu rendern. Danach muss es die übermittelten Daten identifizieren können und, wenn es OK ist, muss es diese mit einer anderen Repository (die nennen wir IAcquisition) abspeichern. Mit diesem “ist OK” wird sichergestellt, dass die obigen Regeln eingehalten wurden, also dass nicht zu viele Produkte bzw. identische Produkte ausgewählt wurden. Danach muss die View die Daten wieder rendern.

Mit diesen Informationen können wir bereits das Produkt und die 2 Interfaces definieren, die wir hier als Blackbox betrachten:

namespace WebformMVP.Tests
{
    //Wegen der Bedingung "nicht mehr als zwei vom selben Typ" muss eine Product-Klasse geben. Sonst würde auch ein string reichen
    public class Product
    {
        public string Name;
        public int Type;
        public Product(string name, int type)
        {
            Name = name;
            Type = type;
        }
    }

    public interface IDeposit
    {
        IList<Product> Load();
    }

    public interface IAcquisition
    {
        void Add(Product product);
    }
}

Jetzt ist endlich Zeit für den ersten Test. Wie ich schon am Anfang geschrieben habe, eine View muss einfach nur Text darstellen. Um die View simulieren zu können, wird sie von einem Interface abgeleitet, genauso wie die Testklasse, unsere Fakeview. Diese bekommt als Felder strings anstelle von Controls, die allerdings korrekt gefüllt werden müssen. Wir tun so als ob, wir abstrahieren die View auf das Minimum:

namespace WebformMVP.Tests
{
    public interface IShoppingView
    {
        void AddSourceItem(string text, bool available);
    }

    [TestFixture]
    public class Tests : IShoppingView
    {
        string m_sourcePanel;
        string m_shoppingCartPanel;

        [Test]
        public void FillSourcePanel()
        {
            m_model.Fill();
            m_model.Render();

            Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
        }
    }
}

So wird die Anwendung natürlich nicht mal kompiliert :), dazu brauchen wir noch ein paar Schritte.

Dadurch, dass die Testklasse von diesem Interface ableitet, sind wir in der Lage, die Methoden entsprechend überschreiben zu können. Dieser Trick nennt sich Implement Interfaces Explicitly. Gleichzeitig lassen wir die Klasse auch von IDeposit ableiten, um auch dessen Methode zu überschreiben:

namespace WebformMVP.Tests
{
    [TestFixture]
    public class Tests : IShoppingView, IDeposit
    {
        string m_sourcePanel;
        string m_shoppingCart;
        IList<Product> m_sources= new List<Product>{new Product("Product 1", 1), new Product("Product 2", 2), new Product("Product 3", 3)};

        void IShoppingView.AddSourceItem(string text, bool available)
        {
            m_sourcePanel += text + (available ? " available;": string.Empty) + " ";
        }

        IList<Product> IDeposit.Load()
        {
            return m_sources;
        }

        [Test]
        public void FillSourcePanel()
        {
            m_model.Fill();
            m_model.Render();

            Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
        }
    }
}

Es funktioniert immer noch nicht, wir brauchen ja noch ein Modell.

namespace WebformMVP.Tests
{
    public class ShoppingModel
    {
        public void Fill()
        {
            throw new NotImplementedException();
        }
        public void Render()
        {
            throw new NotImplementedException();
        }
    }

    [TestFixture]
    public class Tests : IShoppingView, IDeposit
    {
        ...
        private ShoppingModel m_model;

        //Es muss sichergestellt werden, dass beim Laden der View alle Felder leer sind.
        [SetUp]
        public void Setup()
        {
            m_sourcePanel = string.Empty;
            m_shoppingCart = string.Empty;
            m_model= new ShoppingModel();
        }

        [Test]
        public void FillSourcePanel()
        {
            m_model.Fill();
            m_model.Render();

            Assert.That(m_sourcePanel, Is.EqualTo("Product 1 available; Product 2 available; Product 3 available; "));
        }
    }
}

Ok, es kompiliert endlich! Aber wir gehen ja nach TDD vor, der Test ist wie gewünscht rot :D. Die 2 Methoden Fill und Render sind noch nicht implementiert.
Was sollen die Methoden tun? Fill() sollte eine lokale Liste mit Hilfe der Deposit-Repository füllen und Render() soll diese Elemente in das SourcePanel-Feld der View schreiben. Also muss unser Modell eine Liste, das IDeposit-Interface und das IShoppingVew als neue Member bekommen. Letzteren werden natürlich injectet (s. Dependency Inversion):

   public class ShoppingModel
   {
      IDeposit m_deposit;
      IList<Product> m_products;
      [NonSerialized]IShoppingView m_view;

      public ShoppingModel(IDeposit deposit)
      {
         m_deposit = deposit;
      }

      public void SetView( IShoppingView view )
      { 
         m_view = view;
      }
      public void Fill()
      {
         m_products = m_deposit.Load();
      }
      public void Render()
      {
         foreach( Product product in m_products )
         {
            m_view.AddSourceItem( product.Name, true );
         }
      }
   }

   [TestFixture]
   public class ShoppingCartTests:IShoppingView,IDeposit
   {
      ...
      private ShoppingModel m_model;

      [SetUp]
      public void Setup()
      {
         m_model = new ShoppingModel(this);
         m_model.SetView( this );
         m_sourcePanel = string.Empty;
         m_cartPanel = string.Empty;
      }

      [Test]
      public void FillSourcePanel()
      {
         m_model.Fill();
         m_model.Render();

         Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 available; Product 2 available; Product 3 available; " ) );
      }
      ...
   }

Der Test ist grün! Jetzt ist sicher klar wie es weitergeht und ich will den Artikel nicht noch länger machen. Hier sind also die nächsten Tests und die Implementierung dazu:

   [TestFixture]
   public class ShoppingCartTests:IShoppingView,IDeposit
   {
      private string m_sourcePanel;
      private string m_cartPanel;
      private IList<Product> m_products = new List<Product> { new Product( "Product 1", 1 ), new Product( "Product 2", 2 ), new Product( "Product 3", 3 ) };
      private ShoppingModel m_model;

      [SetUp]
      public void Setup()
      {
         m_model = new ShoppingModel(this, new Cart());
         m_model.SetView( this );
         m_sourcePanel = string.Empty;
         m_cartPanel = string.Empty;
      }
...
      [Test]
      public void AddAnItem()
      {
         m_model.Fill();
         m_model.AddAt( 0 );
         m_model.Render();

         Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 available; Product 2 available; Product 3 available; " ) );
         Assert.That( m_cartPanel, Is.EqualTo( "Product 1 " ) );
      }

      [Test]
      public void AddTwoItemsOfAKind()
      {
         m_model.Fill();
         m_model.AddAt( 0 );
         m_model.AddAt( 0 );
         m_model.Render();

         Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 Product 2 available; Product 3 available; " ) );
         Assert.That( m_cartPanel, Is.EqualTo( "Product 1 Product 1 " ) );
      }

      [Test]
      public void AddThreeDifferentItems()
      {
         m_model.Fill();
         m_model.AddAt( 0 );
         m_model.AddAt( 2 );
         m_model.AddAt( 1 );
         m_model.Render();

         Assert.That( m_sourcePanel, Is.EqualTo( "Product 1 Product 2 Product 3 " ) );
         Assert.That( m_cartPanel, Is.EqualTo( "Product 1 Product 3 Product 2 " ) );
      }
...
      void IShoppingView.AddCartItem( string text )
      {
         m_cartPanel += text + " ";
      }
   }
   public class ShoppingModel
   {
      IDeposit m_deposit;
      IList<Product> m_products;
      ICart m_cart;
      [NonSerialized] IShoppingView m_view;

      public ShoppingModel(IDeposit deposit, ICart cart)
      {
         m_deposit = deposit;
         m_cart = cart;
         m_view = view;
         m_products = new List<Product>();
      }
      public void SetView( IShoppingView view )
      { 
         m_view = view;
      }

      public void Fill()
      {
         m_products = m_deposit.Load();
      }

      public void Render()
      {
         foreach( Product product in m_products )
         {
            m_view.AddSourceItem( product.Name, m_cart.IsOkToAdd(product) );
         }
         foreach( Product product in m_cart.List )
         {
            m_view.AddCartItem( product.Name );
         }
      }

      public void AddAt( int index )
      {
         var product = m_products[index];
         m_cart.Add( product );
      }
   }

   public interface ICart
   {
      bool IsOkToAdd( Product product );
      void Add( Product product );
      IList<Product> List { get; }
   }

   public class Cart :ICart{

      private IList<Product> m_cartItems = new List<Product>();

      public bool IsOkToAdd( Product product )
      {
         return m_cartItems.Where( a => a.Type == product.Type ).Count() < 2 && m_cartItems.Count < 3;
      }

      public void Add( Product product )
      {
         m_cartItems.Add( product );
      }

      public IList<Product> List
      {
         get { return m_cartItems; }
      }
   }

Die einzige größere Änderung zum ersten Test ist das neue ICart-Objekt. Da es hier um mehr als es eine Liste geht (irgendwo muss ja die Logik der maximal 2 gleichen Produkte pro Warenkorb errechnet werden), habe ich dafür das Interface und die Klasse definiert.

Jetzt sind wir fast fertig. Es muss lediglich die abstrahierte Umgebung in eine Webanwendung nachgebaut werden. Das heißt, wir implementieren die Methoden Page_Load(), Pre_Render() und AddAt_Click() und die Methoden des Interface IShoppingView. Um die Kontrolle zu behalten, löschen wir den Designer und schalten den ViewState aus (deswegen mag ich diesen Ingemar so sehr 😉 ).

//default.aspx
<%@ Page Language="C#" AutoEventWireup="true" EnableViewState="false" CodeBehind="Default.aspx.cs" Inherits="ShoppingCart.Web._Default" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Shopping Cart</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:Panel runat="server" ID="srcPanel"></asp:Panel>
    <asp:Panel runat="server" ID="cartPanel"></asp:Panel>
    </div>
    </form>
</body>
</html>

//default.aspx.cs
using System;
using System.Collections.Generic;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace ShoppingCart.Web
{
   public class _Default : System.Web.UI.Page, IShoppingView
   {
      protected ShoppingModel model;
      protected HtmlTable srcTable, cartTable;
      protected Panel srcPanel, cartPanel;

      protected void Page_Load( object sender, EventArgs e )
      {
         if( !IsPostBack )
         {
            model = new ShoppingModel(new FakeDeposit(),new Cart());
            //Speichern, hier in Session aber sonst natürlich mit einer Repository
            Session["ShoppingModel"] = model;
            model.Fill();
         }
         else
         {
            model = (ShoppingModel)Session["ShoppingModel"];
         }
         model.SetView( this );
         ModelRender();
      }

      protected void Page_PreRender()
      {
         srcPanel.Controls.Clear();
         cartPanel.Controls.Clear();
         ModelRender();
      }

      private void ModelRender()
      {
         srcTable = new HtmlTable();
         srcPanel.Controls.Add( srcTable );
         srcTable.Width = "50%";
         srcTable.Border = 1;

         cartTable = new HtmlTable();
         cartPanel.Controls.Add( cartTable );
         cartTable.Width = "50%";
         cartTable.Border = 1;
         cartTable.BgColor = "#cccccc";

         model.Render();
      }
      public void AddSourceItem( string text, bool available )
      {
         int index = srcTable.Rows.Count;
         HtmlTableRow tr = new HtmlTableRow();
         HtmlTableCell tc = new HtmlTableCell { InnerText = text };
         if( available )
         {
            LinkButton lb = new LinkButton();
            tc.Controls.Add( lb );
            lb.ID = index.ToString();
            lb.Text = ">>";
            lb.Click += AddAt_Click;
         }
         tr.Cells.Add( tc );
         srcTable.Rows.Add( tr );
      }
      private void AddAt_Click( object sender, EventArgs e )
      {
         model.AddAt( Convert.ToInt32( ((LinkButton)sender).ID ) );
      }
      public void AddCartItem( string text )
      {
         HtmlTableCell tc = new HtmlTableCell { InnerText = text };
         HtmlTableRow tr = new HtmlTableRow();
         tr.Cells.Add( tc );
         cartTable.Rows.Add( tr );
      }
   }

   internal class FakeDeposit :IDeposit
   {
      public IList<Product> Load()
      {
         return new List<Product> { new Product( "Product 1", 1 ), new Product( "Product 2", 2 ), new Product( "Product 3", 3 ) };
      }
   }
}

Fertig. Ich muss eingestehen, als ich das Beispiel aus dem Buch nachprogrammiert habe, war ich wirklich überrascht, wie alles geklappt hat, obwohl ich während des Testens keine Webseite angesprochen habe. Die Wahrheit ist, ich habe noch nie nach dem MVP-Pattern entwickelt, aber eine Webseite so aufzusetzen ist genial! Hoch lebe die Abstraktion!

Ich lade hier das Projekt hoch, vielleicht glaubt es mir jemand nicht 😉

Jeder Code ist testbar

Immer wieder steht man vor der Herausforderung, zu einem bestehenden Legacy-Code neue Funktionalitäten hinzu zufügen oder vorhandene Bugs beheben zu müssen.
Ich möchte hier eine Möglichkeit dazu beschreiben, einen Weg den ich u.a. von Michael Feathers gelernt habe.

Nehmen wir an, der Code schaut so aus:

using System.Net.Mail;

namespace LegacyCode
{
   public class KompletterWorkflow
   {
      public void SpeichereDatenUndVersendeMail( int id, string emailaddress, string text, string cc )
      {
         if( !string.IsNullOrEmpty( text ) )
         {
            Daten user;
            DatenRepository repository = new DatenRepository();
            user = new Daten { Id = id, Email = emailaddress };
            repository.Save( user );

            if( !SmtpRepository.IsValidEmailAddress( emailaddress ) ) return;
            IEmailRepository emailrep = new SmtpRepository();
            MailMessage message = new MailMessage( "myAddress@xy.com", emailaddress, "Testmail", text );
            if( !string.IsNullOrEmpty( cc ) ) message.CC.Add( cc );
            emailrep.SendMail( message );
         }
      }
   }
}

Die referenzierten Klassen wären dann sowas wie:

   public class DatenRepository
   {
      public void Save( Daten daten )
      {
         //Speichert die Daten ab
      }
   }

   public interface IEmailRepository
   {
      void SendMail( MailMessage message );
   }

   public class SmtpRepository : IEmailRepository
   {
      public static bool IsValidEmailAddress( string address )
      {
         try
         {
            MailAddress mailAddress = new MailAddress( address );
            return true;
         }
         catch
         {
            return false;
         }
      }

      public void SendMail( MailMessage message )
      {
         //Versende die Email
      }
   }

   public class Daten
   {
      public int Id { get; set; }
      public string Email { get; set; }
   }

Diese Klasse, so wie sie ist, verletzt jede Menge Prinzipien.
Die wichtigsten sind: The Single Responsibility Principle (SRP) und The Open-Closed Principle (OCP). Außerdem ist die Klasse in diesem Zustand nicht testbar, da man mittendrin Objekte erstellt, deren Verhalten auch mitgetestet werden müssten.
Wenn man das weißt, dann ist die Aufgabe einfach: die Implementierungen DatenRepository und SmtpRepository dürfen nicht in der Methode instanziert werden, sondern sie müssen nach der Regeln der Inversion of Control (IoC) der Klasse übergeben werden.
Zusätzlich werden auch die zusammenhängenden Parameter der Methode SpeichereDatenUndVersendeMail zum userDaten zusammengefasst.

   public class KompletterWorkflow
   {
      private DatenRepository m_repository;
      private IEmailRepository m_emailrep;

      public KompletterWorkflow(DatenRepository repository, IEmailRepository emailrep)
      {
         m_repository = repository;
         m_emailrep = emailrep;
      }

      public void SpeichereDatenUndVersendeMail( Daten userDaten, string text, string cc )
      {
         if( !string.IsNullOrEmpty( text ) )
         {
            m_repository.Save( userDaten);

            if( !SmtpRepository.IsValidEmailAddress( userDaten.Email ) ) return;
            MailMessage message = new MailMessage( "myAddress@xy.com", userDaten.Email, "Testmail", text );
            if( SmtpRepository.IsValidEmailAddress( cc ) ) message.CC.Add( cc );
            m_emailrep.SendMail( message );
         }
      }
   }

Um SRP gerecht zu werden, müsste man auch die Methode in 2 teilen: DatenSpeichern und VersendeMail. Das ist allerdings aus der Sicht der Testbarkeit unwichtig und ich will den Artikel nicht unnötig in die Länge ziehen 😉

Jetzt müssen wir nur noch die Tests schreiben.
Da wir Unittests schreiben, also nur das Verhalten dieser eine Methode testen, müssen wir die fremden Objekte mocken: Das hbedeutet, wir werden ihr Verhalten nachspielen, so tun als ob.

Wir müssen 3 verschiedene Fälle behandeln: ein Interface IEmailRepository, eine konkrete Implementierung DatenRepository und eine statische Methode SmtpRepository.IsValidEmailAddress(string). Bei den ersten zwei empfehlt es sich ein Mocking-Framework wie z.B. RhinoMock zu nutzen.

using NUnit.Framework;
using Rhino.Mocks;
using System.Net.Mail;
namespace LegacyCode.Tests
{
   [TestFixture]
   public class DatenSpeichernUndVersendeMailTests
   {
      private IEmailRepository m_emailrep;
      private DatenRepository m_repository;

      [SetUp]
      public void Init()
      {
         m_emailrep = MockRepository.GenerateStub<IEmailRepository>();
         m_repository = MockRepository.GenerateStub<DatenRepository>();
      }

      [Test]
      public void Daten_werden_gespeichert_wenn_Text_nicht_leer()
      {
         //Arrange
         KompletterWorkflow workflow = new KompletterWorkflow( m_repository, m_emailrep );
         Daten userDaten = new Daten{Id=1,Email="test@test.de"};

         //Act
         workflow.SpeichereDatenUndVersendeMail( userDaten , "Emailtext", "cc" );

         //Assert
         m_repository.AssertWasCalled( a => a.Save( userDaten ) );
         m_emailrep.AssertWasCalled( a => a.SendMail( Arg<MailMessage>.Matches( b => b.To[0].Address == userDaten.Email && b.CC.Count == 0 ) ) );
      }
   }
}

Wenn wir jetzt die Tests mit NUnit ausführen, bekommen wir folgende Fehlermeldung:

LegacyCode.Tests.DatenSpeichernUndVersendeMailTests.Daten_werden_gespeichert_wenn_Text_nicht_leer:
System.InvalidOperationException : No expectations were setup to be verified, ensure that the method call in the action is a virtual (C#) / overridable (VB.Net) method call

Das heißt, die Methoden, worüber wir Annahmen treffen, müssen entweder in einem Interface veröffentlicht worden oder überschreibbar also virtual sein.

      public virtual void Save( Daten daten )
      {
         //Speichert die Daten ab
      }

Wenn wir jetzt die Tests durchführen, dann passt alles.

Für die statische Methode erstellt man am besten eine separate Testklasse. Da hier keine andere Komponenten wie Datenbank oder Filesystem angesprochen werden, wird die Methode ganz einfach zu testen sein:

   [TestFixture]
   public class IsValidEmailTests
   {
      [Test]
      public void Leere_Adresse_ist_nicht_valide()
      {
         Assert.That( SmtpRepository.IsValidEmailAddress( string.Empty ), Is.False );
      }
   }

Das war’s !
Mit weiteren Tests kann das Verhalten unserer Klasse komplett abgedeckt und danach mit TDD die Klasse erweitert oder verändert werden.
Und das alles ohne die Befürchtung, dass diese Änderungen zu unvorhersehbaren Ergebnissen führen.