Archive for the “Unit Testing” Category

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

Comments No Comments »

Wir fragen uns seit langem, wie man internal Methoden testen kann, da eins klar ist, die Kapselung darf nicht wegen der Testfähigkeit verletzt werden.
Letzte Woche hatte zum Glück auch jemand anderer dieses Problem und er hat gleich bei CCD-GoogleGroup nachgefragt. Und so haben wir auch erfahren, wie die Lösung lautet:

für “internal”-Elemente gibt es auch die Option mit [assembly:
InternalsVisibleTo(“TestAssembly”)] zu arbeiten. Alternativ kannst du die zu
testenden Klassen auch per “Add existing item” und dann “Add as link” (siehe
kleines Dreieck neben dem “Add”-Button) zum Testprojekt hinzufügen.

(Danke Alex)

Danach war nur noch ein wenig Surfen nötig, um alles zu erfahren:

msdn sagt:

InternalsVisibleToAttribute Class

Specifies that types that are ordinarily visible only within the current assembly are visible to another assembly.

User comment:
It is not documented anywhere to my knowledge, but if you want to grant “InternalsVisibleTo” permission to more than one assembly, you need to understand the syntax.

To do this you should NOT insert multiples instances of:

[assembly: InternalsVisibleTo("FirstAssembly")]

Instead do this:

[assembly: InternalsVisibleTo("FirstAssembly"),
InternalsVisibleTo("SecondAssembly"),
InternalsVisibleTo("ThirdAssembly")]

The former syntax is legal but fails, because each instance simply redefines and replaces any earlier ones, the latter syntax works as required.

kick it on dotnet-kicks.de

Comments 1 Comment »