Posts Tagged “Unit-Tests”

Der vorherige DbC-Artikel ist ziemlich “abstrakt” ausgefallen, es haben einfach Beispiele gefehlt. Das möchte ich hiermit nachholen.

Erstens muss man die IDE anpassen: im März kommt .NET 4.0 raus und da wird Design by Contract mitgeliefert. Man kann das Konzept aber jetzt schon anwenden, wenn man die Assembly zusätzlich installiert. Danach muss man die dll referenzieren und im Eigenschaftenfenster des Projektes im neuen Tab Code Contracts das Runtime Checking einstellen.

Jetzt zum Code: Nehmen wir eine ganz einfache Klasse Bill deren Objekte mit einem IRepository gespeichert bzw. geladen werden.

    1 using System.Diagnostics.Contracts;

    2 namespace ContractsPrototyp

    3 {

    4     public class Bill

    5     {

    6         public int Id { get; set; }

    7         public string Number { get; set; }

    8         public double Value { get; set; }

    9     }

   10 

   11

   12     public interface IRepository

   13     {

   14         Bill GetBill(string number);

   15         void SaveBill(Bill bill);

   16     }

Die Kontrakte kann man in den einzelnen Methoden oder für eine ganze Klasse schreiben (unter dem Attribut ContractInvariantMethode) aber ich finde am schönsten, dass man die auch auslagern kann: durch eine gegenseitige Markierung können Kontrakt-Klassen und Interfaces als “Paare” definiert werden:

   11     [ContractClass(typeof(RepositoryContracts))]

   12     public interface IRepository

   13     {

   14         Bill GetBill(string number);

   15         void SaveBill(Bill bill);

   16     }

   17     [ContractClassFor(typeof(IRepository))]

   18     public class RepositoryContracts:IRepository

   19     {

   20         public Bill GetBill(string number)

   21         {

   22             Contract.Requires(!string.IsNullOrEmpty(number));

   23             return null;

   24         }

   25 

   26         public void SaveBill(Bill bill)

   27         {

   28             Contract.Ensures(bill.Id > 0);

   29         }

   30     }

Eine Vorbedingung wird mit Contract.Requires und eine Nachbedingung mit Contract.Ensures definiert. Beide Methoden bekommen boolische Ausdrücke. Diese Ausdrücke müssen frei von Seiteneffekten sein.

Die eigentliche Implementierung der Klasse schaut dann so aus:

   31     public class Repository:IRepository

   32     {

   33         public Bill GetBill(string nummer)

   34         {

   35             //Würde das Objekt aus Datenhaltung laden

   36             return new Bill();

   37         }

   38 

   39         public void SaveBill(Bill bill)

   40         {

   41             //Würde das Objekt speichern und ihm eine Id zuweisen

   42             if (BillIsValid( bill )) bill.Id++;

   43         }

   44 

   45         private static bool BillIsValid(Bill bill)

   46         {

   47             return !string.IsNullOrEmpty(bill.Nummer);

   48         }

   49     }

Woher können wir wissen, dass das funktioniert? Es ist einfach, wir schreiben ein Paar Tests dazu!
Bei Kontraktverletzung wird eine Exception geworfen. Um diese – und dadurch die genaue Verletzung – überprüfen zu können braucht man etwas Workaround:

   55     [TestFixture]

   56     public class BillTests

   57     {

   58         private IRepository m_repository;

   59         private string m_message;

   60 

   61         [SetUp]

   62         public void Setup()

   63         {

   64             m_repository = new Repository();

   65             m_message = string.Empty;

   66             Contract.ContractFailed += ( sender, e ) =>

   67             {

   68                 e.SetUnwind();

   69                 m_message = e.Message;

   70             };

   71         }

Danach sind die Tests dann einfach:

   73         [Test]

   74         public void Laden_mit_leerer_Nummer_verletzt_Kontrakt()

   75         {

   76 

   77             try

   78             {

   79                 m_repository.GetBill( null );

   80             }

   81             catch

   82             {

   83                 //Nichts

   84             }

   85 

   86             Assert.That( m_message, Is.EqualTo( "Precondition failed: !string.IsNullOrEmpty(number)" ) );

   87         }

   88 

   89         [Test]

   90         public void Speichern_Rechnung_ohne_Nummer_verletzt_Kontrakt()

   91         {

   92 

   93             try

   94             {

   95                 m_repository.SaveBill( new Bill{Value = 25} );

   96             }

   97             catch

   98             {

   99                 //Nichts

  100             }

  101 

  102             Assert.That( m_message, Is.EqualTo( "Postcondition failed: bill.Id > 0" ) );

  103         }

Ich hoffe, das Beispiel ist ausführlich genug, um die Vorteile von DbC zu highlighten. Stefan, vielen dank noch mal für den Artikel, ich habe mich natürlich von dir inspirieren lassen.

kick it on dotnet-kicks.de

Share

Comments 2 Kommentare »

Obwohl die allgemeine Meinung ist, dass es sehr schwierig sei, kann man mit folgendem Workaround Webforms sehr einfach und sehr umfangreich testen. Bedingung 1: als Projekt kein Web Site Project sondern eine Web Application erstellen. Bedingung 2: Business Logic in die entsprechende Schicht auslagern.

Nehmen wir zum Beispiel ein einfaches Formular. Nach Absenden des Formulars sollen die Werte aus den 2 Feldern addiert werden. Wenn man per QueryString einen Parameter multiple übergibt, soll das Ergebnis damit multipliziert werden.

Testable WebForm   Testable WebForm Sent

Und nun zum Quellcode: Die automatisch erstellte .designer.cs muss entfernt werden, was man sowieso tun sollte, da man automatisch erstellten Code – also Code, den keiner außer Microsoft unter Kontrolle hat – vermeiden sollte.
Die Inhalte der .designer.cs – also die Definitionen der Web-Elemente – werden in der Klasse als Public Properties erstellt und instantiiert, um bei Zugriffen wie TextBox.Text keine NullReferenceException zu bekommen.

    1 using System;

    2 using System.Web.UI.HtmlControls;

    3 using System.Web.UI.WebControls;

    4 using framework.Testable.Web.UI;

    5 

    6 namespace TestableWebForm

    7 {

    8     public class DefaultPage : System.Web.UI.Page

    9     {

   10 

   11         #region Controls

   12         public HtmlForm Formular;

   13         public TextBox Value1 = new TextBox();

   14         public TextBox Value2 = new TextBox();

   15         public Label Result = new Label();

   16         public Button Submit = new Button();

   17         #endregion

Um das Verhalten testen zu können, haben wir Adapter für die Klassen System.Web.UI.Page, System.Web.HttpRequest und System.Web.HttpResponse geschrieben, und zwar für die Properties und Methoden die uns vorerst interessieren: z.B. Page.IsPostBack, Page.Request, Response.Redirect(string url, bool endResponse). Bei der Benennung haben wir einfach den Namespace System mit framework.Testable ersetzt und wir haben natürlich zu jedem Testable-Objekt einen Interface erstellt.

    1 

    2 namespace framework.Testable.Web

    3 {

    4     namespace UI

    5     {

    6         public interface IPage

    7         {

    8             bool IsPostBack{ get; }

    9             IHttpRequest Request{ get; set; }

   10             IHttpResponse Response{ get; set; }

   11         }

   12 

   13         public class Page : IPage

   14         {

   15             private readonly System.Web.UI.Page m_page;

   16             private IHttpRequest m_request;

   17             private IHttpResponse m_response;

   18 

   19             public Page( System.Web.UI.Page page )

   20             {

   21                 m_page = page;

   22                 m_request = new HttpRequest( m_page );

   23                 m_response = new HttpResponse( m_page );

   24             }

   25 

   26             public bool IsPostBack

   27             {

   28                 get{ return m_page.IsPostBack; }

   29             }

   30 

   31             public IHttpRequest Request

   32             {

   33                 get{ return m_request; }

   34                 set { m_request = value; }

   35             }

   36 

   37             public IHttpResponse Response

   38             {

   39                 get{ return m_response; }

   40                 set { m_response = value; }

   41             }

   42         }

   43     }

   44 }

Um alle gemockte Objekte setzen zu können, haben wir unserer Page-Klasse auch Setter für Request und Response gegeben. Da man Request.Params nicht setzen kann, d.h. Request.Params[] immer ein NullReferenceException verursachen würde, haben wir das Auslesen der Request-Parameter in eine Methode Request.GetParamValue(string name) ausgelagert.

Das war ungefähr alles: in der Seite nutzt man dann anstelle der eigenen Request und Response-Objekten die Testable-Objekte.

    8     public class DefaultPage : System.Web.UI.Page

    9     {

   …

   19         private IPage m_page;

   20         public void SetTestableObjects( IPage page )

   21         {

   22             m_page = page;

   23         }

   24 

   25         public void Page_Load( object sender, EventArgs e )

   26         {

   27             if (m_page == null) m_page = new Page( this );

   28             int multiple = 1;

   29             if (!string.IsNullOrEmpty( m_page.Request.GetParamValue( "multiple" ) )) multiple = Convert.ToInt32( m_page.Request.GetParamValue( "multiple" ) );

   30             if (m_page.IsPostBack)

   31             {

   32                 Result.Text = ( (Convert.ToInt32( Value1.Text ) + Convert.ToInt32( Value2.Text ))*multiple ).ToString();

   33             }

   34         }

Damit ist die Web-Anwendung bereit zum Testen. So schaut zum Beispiel ein Test für das Laden der Seite aus:

    1 using framework.Testable.Web;

    2 using framework.Testable.Web.UI;

    3 using NUnit.Framework;

    4 using Rhino.Mocks;

    5 using TestableWebForm;

    6 

    7 namespace Tests

    8 {

    9     [TestFixture]

   10     public class WebFormTests

   11     {

   12         private IPage m_page;

   13         private IHttpRequest m_request;

   14         private IHttpResponse m_response;

   15         private DefaultPage m_defaultPage;

   16 

   17         [SetUp]

   18         public void Init()

   19         {

   20             m_page = MockRepository.GenerateStub<IPage>();

   21             m_request = MockRepository.GenerateStub<IHttpRequest>();

   22             m_response = MockRepository.GenerateStub<IHttpResponse>();

   23             m_page.Request = m_request;

   24             m_page.Response = m_response;

   25         }

   26 

   27         [Test]

   28         public void PageLoad_Loading_EmptyFields()

   29         {

   30             // Arrange

   31 

   32 

   33             // Act

   34             m_defaultPage = new DefaultPage();

   35             m_defaultPage.SetTestableObjects(m_page);

   36             m_defaultPage.Page_Load( null, null );

   37 

   38             // Assert

   39             Assert.IsEmpty( m_defaultPage.Value1.Text );

   40             Assert.IsEmpty( m_defaultPage.Value2.Text );

   41             Assert.IsEmpty(m_defaultPage.Result.Text);

   42         }

Und so für PostBack inklusive QueryString-Parameter:

   63         [Test]

   64         public void PageLoad_PostBackWithRequestValue_ResultIsCorrect()

   65         {

   66             // Arrange

   67             const int value1 = 1;

   68             const int value2 = 2;

   69             const int value3 = 3;

   70             m_request.Expect( a => a.GetParamValue( "multiple" ) ).IgnoreArguments().Repeat.Twice().Return( value3.ToString() );

   71             m_page.Expect( a => a.IsPostBack ).Return( true );

   72 

   73             // Act

   74             m_defaultPage = new DefaultPage();

   75             m_defaultPage.SetTestableObjects( m_page );

   76             m_defaultPage.Value1.Text = value1.ToString();

   77             m_defaultPage.Value2.Text = value2.ToString();

   78             m_defaultPage.Page_Load( null, null );

   79 

   80             // Assert

   81             Assert.IsTrue( m_defaultPage.Result.Text == ( (value1 + value2)*value3 ).ToString() );

   82         }

Dieses Vorgehen hat uns nicht nur den seit langen gesuchten Weg zum Testen von Webanwendungen geebnet, sondern zwingt auch den Entwickler dazu, alle Funktionalitäten, die nicht in einer Webseite sondern in die dll-s gehören, auszulagern. Damit dürfte es auch der richtige Weg der Clean Code Developers für die Arbeit mit WebForms sein.

Was die Adapter-Klassen betrifft: inzwischen haben wir auch System.IO “adaptiert” und bald werden die anderen System-Klassen folgen, je nach Bedarf.
Download VS2008-Projekt

kick it on dotnet-kicks.de

Share

Comments Keine Kommentare »