Kontextabhängige Datenvalidierung

Die Idee stammt von Jimmy Nilsson – Applying Domain-Driven Design and Patterns. Er hat nach einer Möglichkeit gesucht, die immer wiederkehrende Aufgabe, Daten zu validieren, flexibel und kontextabhängig zu gestalten, und zwar so, dass man es nur einmal schreiben muss.

Wie läuft normalerweise so eine Validierung ab? Man will wissen, ob eine Instanz als solche allen Vorschriften entspricht, und wenn nicht, dann welche Felder passen nicht. Jeder, der jemals Webanwendungen geschrieben hat, weiß, wie mühsam und langweilig es ist, jeden Eingabewert auf Gültigkeit zu testen. (Hier geht allerdings nicht unbedingt um Formulare, da kann man ja die Validierung z.B. bei ASP.MVC 2.0 mit Data Annotations durchführen.)

Also zurück zu den Anforderungen: um die verschiedenen Regeln wiederverwendbar zu machen, braucht man diese von einem Interface abzuleiten:

namespace ValidationFramework
{
    public interface IRule
    {
        bool IsValid { get; }
        int IdRule { get; }
        string[] BooleanFieldsThatMustBeTrue { get; }
        string Message { get; }
    }
}

IsValid sagt aus, ob der Regel verletzt wurde. Die IdRule ist dafür da, um diese Regeln einfacher identifizieren zu können. BooleanFieldsThatMustBeTrue kann dafür verwendet werden, um Vorbedingungen zu prüfen. Message braucht wohl keine Erklärung.
Jetzt kann man verschiedene Regeln und eine Basisklasse für gemeinsame Funktionalitäten definieren:

using System.Collections.Generic;
using System.Linq;
using System;

namespace ValidationFramework
{
    public abstract class RuleBase : IRule
    {
        private readonly object m_value;
        private readonly int m_idRule;
        private readonly string[] m_booleanFieldsThatMustBeTrue;
        private readonly object m_holder;

        protected RuleBase(int idRule, string[] fieldsConcerned, string fieldname, object holder)
        {
            m_value = GetPropertyValue(holder, fieldname);
            m_booleanFieldsThatMustBeTrue = fieldsConcerned;
            m_holder = holder;
            m_idRule = idRule;
        }

        private static object GetPropertyValue(object holder, string fieldname)
        {
            return holder.GetType().GetProperty(fieldname).GetValue(holder, null);
        }

        protected object GetValue()
        {
            return m_value;
        }
        protected object GetHolder()
        {
            return m_holder;
        }

        public abstract bool IsValid { get; }
        public int IdRule
        {
            get { return m_idRule; }
        }

        public string[] BooleanFieldsThatMustBeTrue
        {
            get { return m_booleanFieldsThatMustBeTrue; }
        }

        public abstract string Message { get; }

        protected bool BooleanFieldsConcernedAreTrue()
        {
            return
                m_booleanFieldsThatMustBeTrue.Select(a => (bool)m_holder.GetType().GetProperty(a).GetValue(m_holder, null)).
                    Select(b => b).Count() == m_booleanFieldsThatMustBeTrue.Length;
        }
    }

    public class DateIsInRangeRule : RuleBase
    {
        private readonly DateTime m_minDate;
        private readonly DateTime m_maxDate;

        public DateIsInRangeRule(DateTime minDate, DateTime maxDate, int idRule, string fieldName, object holder) : base( idRule, null,fieldName, holder)
        {
            m_minDate = minDate;
            m_maxDate = maxDate;
        }

        public override bool IsValid
        {
            get { 
                var value = (DateTime) GetValue();
                return value >= m_minDate && value <= m_maxDate;
            }
        }

        public override string Message
        {
            get {
                return IsValid
                           ? string.Empty
                           : string.Format("Das Datum ist nicht in gültigen Bereich: {0}-{1}", m_minDate, m_maxDate); }
        }
    }

    public class MaxStringLengthRule : RuleBase
    {
        private readonly int m_maxLength;

        public MaxStringLengthRule(int maxLength, int idRule, string fieldname, object holder) : base(idRule, null, fieldname, holder)
        {
            m_maxLength = maxLength;
        }

        public override bool IsValid
        {
            get { return GetValue().ToString().Length <= m_maxLength; }
        }

        public override string Message
        {
            get {
                return IsValid
                           ? string.Empty
                           : string.Format("Die zugelassene Länge von {0} Zeichen wurde überschritten",m_maxLength); }
        }
    }
}

Jetzt, da die Grundlagen stehen, schauen wir mal, wie man die Validierungsregeln festlegen würde.
Hier ist eine ganz einfache Beispielklasse:

using System;

namespace ValidationFramework
{
    public class Account
    {
        public Account()
        {
            Created = DateTime.Now;
            Address = string.Empty;
        }

        public DateTime Created { set; get; }
        public string Address { get; set; }
        public bool Activated { get; set; }
    }
}

Jetzt definieren wir die Regeln, wonach eine Instanz valide ist oder nicht. Die neue Eigenschaft IsValidated soll diese Information speichern.

    public class Account
    {
        private readonly IList<IRule> m_validationRules = new List<IRule>();

        public Account()
        {
            Created = DateTime.Now;
            Address = string.Empty;
        }

        public DateTime Created { set; get; }
        public string Address { get; set; }
        public bool Activated { get; set; }
        public bool IsValidated { get; set; }

        private void SetupValidationRules()
        {
            m_validationRules.Add(new DateIsInRangeRule(new DateTime(1990,1,1),DateTime.Now,1,"Created",this ));
            m_validationRules.Add(new MaxStringLengthRule(10,2,"Address",this));
        }
    }

Um Regeln auch dynamisch setzen zu können, wird eine Methode AddValidationRule(IRule) definiert

 
    public class Account
    {
...    
        public void AddValidationRule(IRule rule)
        {
            m_validationRules.Add(rule);
        }
...
    }

Nun müssen wir diese Regeln nur noch auswerten. Dafür wird in RuleBase eine statische Methode definiert und in der Beispielklasse die Methode IEnumerable GetBrokenRules()

    public abstract class RuleBase : IRule
    {
...
        public static IEnumerable<IRule> CollectBrokenRules(IList<IRule> rulesToCheck)
        {
            return rulesToCheck.Where(a => !a.IsValid).Select(a => a);
        }
    }
    public class Account
    {
...
        public IEnumerable<IRule> GetBrokenRules()
        {
            SetupValidationRules();
            return RuleBase.CollectBrokenRules(m_validationRules);
        }
...
    }

Um zu beweisen, dass es funktioniert, hier ein Paar Tests:

using System;
using System.Linq;
using NUnit.Framework;

namespace ValidationFramework.Tests
{
    [TestFixture]
    [Category("DateIsInRangeRule")]
    public class If_the_CreationDate_of_the_Account_is_in_range
    {
        [Test]
        public void Then_the_property_Created_is_valid()
        {
            var sut = new Account { Created = DateTime.Now.AddYears(-1) };
            Assert.That(sut.GetBrokenRules().Count(),Is.EqualTo(0));
        }
    }
    [TestFixture]
    [Category("DateIsInRangeRule")]
    public class If_the_CreationDate_of_the_Account_is_to_old
    {
        [Test]
        public void Then_the_property_Created_is_not_valid()
        {
            var sut = new Account { Created = new DateTime(1989,1,1)};
            Assert.That(sut.GetBrokenRules().Count(), Is.EqualTo(1));
            Assert.That(sut.GetBrokenRules().First().IdRule, Is.EqualTo(1));
        }
    }

    [TestFixture]
    [Category("MaxStringLengthRule")]
    public class If_the_Address_of_the_Account_is_to_long
    {
        [Test]
        public void Then_the_property_Address_is_not_valid()
        {
            var sut = new Account { Address = "12345678901"};
            Assert.That(sut.GetBrokenRules().Count(), Is.EqualTo(1));
            Assert.That(sut.GetBrokenRules().First().IdRule,Is.EqualTo(2));
        }
    }
}

Mit den vorhandenen Tests kann man nun refaktorisieren um die Prinzipien der Separation Of Concern einzuhalten. Außerdem ist nun Zeit, auch über die Kontextbezogenheit nachzudenken.
Die Klasse Account wird wahrscheinlich durch irgendeinen ORMapper gefüllt und braucht auf keinem Fall die Verantwortung, die Validierungsregeln zu verwalten. Deshalb kann man das Specification-Pattern von DDD anwenden, und diese Regeln in so eine Spezifikation verschieben:

using System;
using System.Collections.Generic;

namespace ValidationFramework
{
    public interface IValidationSpecification
    {
        IList<IRule> GetValidationRules();
    }

    public class AccountValidationSpecification : IValidationSpecification
    {
        private readonly Account m_objectToValidate;

        public AccountValidationSpecification(object objectToValidate)
        {
            m_objectToValidate = (Account) objectToValidate;
        }

        public IList<IRule> GetValidationRules()
        {
            return new List<IRule>
                       {
                           new DateIsInRangeRule(new DateTime(1990, 1, 1), DateTime.Now, 1, "Created",
                                                 m_objectToValidate),
                           new MaxStringLengthRule(10, 2, "Address", m_objectToValidate)
                       };
        }
    }
}

Die Spezifikationen werden selbstverständlich von einer Factory geliefert und sie werden per Setter Injection gesetzt.

    public  class ValidationSpecificationFactory
    {
        public static IValidationSpecification Create<T>(object objectToValidate)
        {
            if (typeof(T) == typeof(Account))
                return new AccountValidationSpecification(objectToValidate);
            throw new NotSupportedException();
        }
    }
    public class Account
    {
...
        public void SetValidationSpecification(IValidationSpecification specification)
        {
            foreach (IRule rule in specification.GetValidationRules())
                m_validationRules.Add(rule);
        }
...
    }
//Der Aufruf ist dann
var account = new Account();
account.SetValidationSpecification(ValidationSpecificationFactory.Create<Account>(account));

Dadurch ist die Bedingung, Kontextabhängige Validierungsregeln festlegen zu können, erfüllt.

Man kann generische Spezifikationen definieren, die die Regeln für verschiedene Zwecke, z.B. für Persistieren oder auch einfach nur für Akzeptieren definieren:

   
    public interface IValidationSpecification
    {
        IList<IRule> GetValidationRules();
        IList<IRule> GetValidationRulesRegardingPersistence();
    }

Hier der komplette Quellcode zum Herunterladen.

NOS Sued kommt: Ulm heißt heuer Karlsruhe

.NET Open Space Süd
Es ist wieder soweit, die Anmeldungen für .NET Open Space Süd haben begonnen. Ich war letztes Jahr in Ulm und es war großartig! Wir haben wahnsinnig viel gelernt und super Leute getroffen. Es stand schon damals außer Frage, ob ich wieder hingehe. Irgendwann in den letzten 2 Wochen kam dann endlich die freudige Nachricht: Ulm ist dieses Jahr in Karlsruhe 🙂
Also kommt hin und werdet Teil der Community, ihr werdet es nicht bereuen!

P.S.: wenn ich die Teilnehmerliste anschaue, muss ich die Frage vom letzten Jahr wiederholen: gibt es keine Entwicklerinnen in diesem Teil des Landes?

Coding Dojo – aber wie?

Wir haben in der Abteilung vor ein paar Wochen eingeführt, am letzten Tag von jedem Sprint einen kleinen Firmeninternen Dojo abzuhalten. Da wir vom Anfang an zu den Münchener Coding Dojos gehen, haben dadurch ziemlich viel Erfahrung gesammelt, wie so ein Dojo durchgeführt wird.

Wie laufen üblicherweise diese Veranstaltungen ab? Der Organisator beschreibt die zur Auswahl stehenden Aufgaben, die Teilnehmer besprechen es kurz, und nachdem die Wahl getroffen wurde, wird fast sofort losgelegt. An der Tastatur. Der erste Test entsteht dann allerdings erst nach 30-40 Minuten. Geplant wird mit einem zuckenden Finger über die Tastatur, der Test entsteht oft nach einem frustrierten Ruf “schreib endlich was, egal was!”. Am Ende jedes Dojos hatten wir dann die Anforderungen erfüllt, aber zur Refactoring kamen wir nie.

Aber wir sind Entwickler, wir entwickeln uns ständig weiter. Seit längerer Zeit spürten wir, dass wir so einen Prozess anders steuern sollten, dass etwas fehlt.

An diesem Freitag haben wir also etwas anderes ausprobiert, sozusagen à la Clean Code: zuerst Anforderungen aufschreiben (das war ja nichts neues), dann nachdenken. Ohne Tastatur, nur am Flipchart. Das war neu. Wir haben die ganze Planung inklusive Funktionseinheiten, Methodennamen, Abhängigkeiten aufgezeichnet.
Danach ging erst das Entwickeln mit TDD los. Nach ca. 90 Minuten waren wir fast fertig und wir haben keine Refactoring mehr gebraucht.

Auf die Frage nach der Meinung den Kollegen, ob das jetzt besser gelaufen war, waren die Antworten geteilt: manche meinten, wir wären auch auf dem anderen Weg zum selben Ergebnis gekommen. Die Wahrheit ist, das Beispiel war recht einfach, es gab nicht sehr viele trennbare Verantwortlichkeiten, aber ich bin sicher, dass man auf dieser Art viel mehr lernen kann. Es reicht ja nicht, eine Sprache zu kennen, ein guter Entwickler muss auch Abstraktionsebenen erkennen und definieren können. Ich finde diese Art zu Arbeiten: zuerst alle Möglichkeiten auf einem Stück Papier zu bewerten und dann sich auf die einzelne Methoden zu konzentrieren, viel effektiver, ich glaube fest daran, dass es zu besserem Code führt.

Also die Frage ist: wie soll eine mit TDD entwickelte Anwendung entstehen? Vom Anfang mit der Tastatur unter den Fingern oder darf es etwas Planung in den Köpfen und auf Papier stattfinden? Haben wir die Regeln von TDD verletzt oder verbessert?

Ich wurde getestet … und ganz gut gefunden!

Ich habe zufällig auf dem Blog von Gordon Breuer eine Webseite entdeckt, wo man seine Kenntnisse teilweise kostenlos testen kann. Ich habe mir den kostenlosen Test “C# 3.0 Fundamentals” ausgewählt und jetzt kann ich hier ganz stolz das Ergebnis melden:

C# 3.0 Fundamentals

Scored higher than 64% of all previous test takers.

Demonstrates a clear understanding of many advanced concepts within this topic. Appears capable of mentoring others on most projects in this area.

Das ist aber ein super Gefühl 🙂
Hier die Bewertung der Punkte:

How do I interpret my Brainbench test score?

Scores range from 1-5 with 5 being the highest in the Brainbench
scoring system. Within that range, Brainbench recognizes two levels
of achievement for standard assessments. A score between 2.75
– 3.99 earns the test taker the standard certification while
a score of 4.0 or higher certifies a master level of achievement.

Scores range from 1 to 5 and are divided into the
following proficiency level categories:

Test Overall Score
Range
Proficiency
Level
1.00 – 1.50 Novice
1.51 – 2.50 Basic
2.51 – 3.50 Proficient
3.51 – 4.50 Advanced Master
4.51 – 5.00 Expert

Während ich den Test gemacht habe, musste ich allerdings ziemlich oft wegen Resharper fluchen, weil der ja die ganze Arbeit macht, wenn es um sowas geht a = b ?? 0; aber bei Multiple Choice hat man ja kein Resharper 😆  (hallo René …)

SOLID Principles

Im vorherigen Artikel habe ich drei der fünf wichtigsten Prinzipien des OOD (Objektorientiertes Design) benannt, ohne mehr darüber zu schreiben. Das würde ich jetzt gerne nachholen.
Diese Prinzipien wurden von Robert C.Martin (a.k.a. Uncle Bob) unter dem Namen S.O.L.I.D. Principles zusammengefasst:

  • SRP: The Single Responsibility Principle
  • OCP: The Open Closed Principle
  • LSP: The Liskov Substitution Principle
  • DIP: The Dependency Inversion Principle
  • ISP: The Interface Segregation Principle

Das Thema wurde von Uncle Bob in mehreren Blogartikeln, Podcasts und vor allem in seinem Buch sehr ausführlich erklärt. Auch andere Entwickler haben darüber geschrieben, zum Beispiel hat Stefan Lieser darüber eine ganze Artikelserie in dotnetpro veröffentlicht.
Ich habe das erste Mal vor einem Jahr in diesem Podcast von Hanselman und Uncle Bob darüber gehört, und während der letzten 12 Monaten wurde ich durch die tägliche Arbeit überzeugt, dass man mit diesen Regeln solide Anwendungen bauen kann.

Was bedeuten also diese Akronyme:

SRP: The Single Responsibility Principle

Die Definition lautet:

A class should have only one reason to change.

Diese Regel ist wahrscheinlich die einfachste und wird wahrscheinlich am meisten verletzt. Wer kennt nicht Klassen, die sowas tun, wie Daten speichern, manipulieren, E-Mails versenden und all das eventuell auch noch loggen. Das war früher eine ganz normale Vorgehensweise. Was passiert aber, wenn die Datenbank-Struktur sich verändert hat? Dann musste man nicht nur diese ändern sondern auch all die Klassen – meistens Verwaltungen oder Manager genannt – die all diese Verantwortlichkeiten hatten. Und darum geht es hier: eine Klasse darf nur einen Grund für Änderungen haben, sie darf nur eine Verantwortlichkeit haben. Also wenn man mehrere Gründe erkennen kann, warum sich eine Klasse verändert, dann wurde dieses Prinzip verletzt. Und das gilt nicht nur für Klassen, sondern auch für andere Funktionseinheiten wie Funktionen, Klassen, Assemblies, Komponenten, alle auf ihre Abstraktionsebene betrachtet.

OCP: The Open Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Jede Funktionseinheit soll erweiterbar sein, also darf nicht zu viele Abhängigkeiten haben, weil die diese Freiheit stark oder ganz einschränken können. Wenn man allerdings ein verändertes Verhalten implementieren will, soll das nicht durch Veränderung des Codes sondern durch hinzufügen von neuen Funktionen passieren.
Das ist nur durch ausreichende Abstraktion zu erreichen. Wenn die Kernfunktionalität in eine abstrakte Basisklasse gekapselt ist, kann man das neue Verhalten in einer abgeleiteten Klasse implementieren.

LSP: The Liskov Substitution Principle

Dieses Prinzip wurde von Barbara Liskov definiert und es lautet so:

Subtypes must be substitutable for their base types.

Einfach übersetzt: jede abgeleitete Klasse einer Basisklasse muss diese Klasse so implementieren, dass sie durch diese jeder Zeit ersetzbar ist. Jedes Mal, wenn man eine Basisfunktion so implementiert, dass diese was ganz anderes tut, als man grundsätzlich erwarten würde, verletzt man dieses Prinzip. Das berühmteste Beispiel ist das Rechteck und das Quadrat. Auf den ersten Blick meint man, dass ein Quadrat ein spezialisiertes Rechteck ist. Was passiert aber, wenn man die Länge oder die Breite eines Quadrates setzt? Es muss jeweils die andere Eigenschaft auch gesetzt werden, sonst wäre es ja kein Quadrat ;). Das ist aber ein Verhalten, was man bei einem Rechteck niemals erwarten würde. Also würde diese Ableitung das Liskovsche Substitutionsprinzip grob verletzen.

DIP: The Dependency Inversion Principle

Die Definition lautet

1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend upon details. Details should depend upon abstractions

Dieses Prinzip ist sehr einfach zu erklären (Beispiel für die Verletzung sieht man ja im vorherigen Artikel): Keine Klasse sollte fremde Klassen instanziieren, sondern diese als Abstraktionen (z.B. Interfaces) in Form eines Parameters bekommen. Das führt dazu, dass die fremde Klasse als Black Box fungieren kann, ihre Veränderungen würden nicht zu Veränderung dieser konkreten Klasse führen.

ISP: The Interface Segregation Principle

Das Prinzip bezieht sich auf “fette” Interfaces:

Clients should not be forced to depend on methods they do not use.

Ein Interface ist der veröffentlichte Kontrakt einer Klasse, eines Moduls. Je mehr Methoden darin registriert wurden, vor allem, wenn sie sehr ähnlich sind oder wenn sie keine selbsterklärende Namen haben, dann ist das eine Zumutung gegenüber der Clients, des Verwenders. Er könnte dazu gezwungen sein, den Code der dahinter stehenden Implementierung anzusehen, was die Erstellung des Interfaces sinnlos macht. Dieses soll ja als Black Box fungieren, nicht als eine Herausforderung für den Entwickler.

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.