Unit-Tests für WebForms
Geschrieben von Christina Hirth in How-To, Unit Testing, Webanwendungen, tags: Unit-Tests, WebapplicationObwohl 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.
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