javascript
Brief description  about Online courses   join in Online courses
OR

Unit Testing ASP.NET

Gautam  Desai
Gautam Desai
Technical Tester

Many developers recently discovered that the Test Driven Development (TDD) process improves the development process significantly. While writing tests is relatively easy in the desktop world, Web applications are not so easily tested. One of the problems is that a Web application runs in a different process, thus being isolated from the test code. As a result, what we are able to test is a raw HTML output, rather than the application's internals. Another problem is that it is very difficult to put a Web application into a special environment that is more suitable for testing. For example, we cannot check the behavior of the system when a database is unavailable, unless we manually stop the database server. Compare this to the desktop unit testing, where we can easily mock the database connection, telling the system to throw an exception without actually trying to connect.
Several Asp.NET frameworks exist, but they are either parsing the HTML output and recreating the structure of the page (NUnitAsp, now discontinued), or test the client-side functionality (Selenium and WatiN). Another framework, Plazma, hosts the Asp.Net runtime so that it is executed in the test runner process; however, due to the runtime's inherent untestability, we are still unable to test, let alone mock, the page's intrinsics. As a result, we can write integration, but not unit, tests. For example, to verify that a certain label displays the user's name, we have to add a user to our database, navigate to the login page, enter the user's credentials, navigate to the page being tested, parse the output in order to find a span with a certain ID, and check the span's inner text. In addition, we have to manually return the database to the previous state.
Frustrated by these complexities, and driven by the common idea that testability equals good code (and vice versa), developers often tend to push as much code from the codebehind files as they can into the presentation logic library, coupling it too much to the view (for example, letting the Presenter handle control events and manipulate controls' properties). The remaining View is left untested, just because it's not worth the effort.
It is clear that developers need to be able to access and manipulate various server-side intrinsics, such as the current HttpContext, the page being requested and its controls, the Session etc. You often want to verify that certain events are being fired in a certain order with certain arguments, or that your custom control is initialized properly at the Init stage. Even more important, sometimes you need to test your page in isolation, so you need to be able to mock the rest of your application. On the other hand, you need some browser-side functionality as well: setting up cookies and checking the return status, filling up form values and pushing buttons etc. This is where the Ivonna framework can help.
Testing with the Ivonna framework is completely different from the client-side frameworks. You are programming in a way similar to desktop testing. You setup your mocks, for example, mock the Membership service, then you request a page (you have a "real" instance of the Page class, not the raw Html), find the label you need and check its Text property. But there's more: you can execute your asserts not only against the response, but also during the page's lifecycle. For example, you can add a Page_Init handler and verify the label's initial value or visibility. You can also run tests against concrete page classes (for example, if you want to test a page-specific method) or App_Code classes.
A simple example
We'll start with a simple task. Let's create a page with a textbox (“NameTextBox”), a button (“SayButton”), and a label (“MessageLabel”). Let's test the following scenario: if the textbox contains “John”, and the button is clicked, the label should display “Hi John!”.
Note that this is an integration test, and we could do the same, or similar, with any other Web testing framework. To demonstrate Ivonna's unit testing capabilities, we'll aim to refactor our code into a View and a Presenter, writing tests for the former (unit testing the Presenter is not a problem, since the class resides in a separate assembly). In particular, we'll write a test for our View that whenever the button is pressed, a certain event is raised. Also, we'll test a View's method and verify that it displays the message on the page. This is, to our knowledge, impossible with any other testing framework.
Setup
The setup process is described in the online documentation. First, you have to ensure thatTypeMock Isolator and Ivonna is installed. You can download Isolator here and Ivonna here. If you want to use an external test runner, you should enable Isolator before you launch the runner (see this link for details). Next, we create a Web site and a test project. We add references to System.Web, MbUnit, TypeMock Isolator, and Ivonna to our test project. Finally, we set the output of our test project to our Web's bin folder.
The first test
The first, integration, test is simple: request a page, enter “John” in the textbox, click the button, and check the label text.
 
ASP.NET Code Example VB.NET
Public Sub ClickingTheButton_DisplaysHiJohn()
    Dim session As New TestSession 'start a browsing session
    Dim page = session.GetPage("Default.aspx") 'request a page
    Dim NameTextBox As TextBox = page.FindControl("NameTextBox") 'find textbox
        NameTextBox.Text = "John" 'enter the text
        page = session.ProcessPostback("SayButton") 'click the button
    Dim MessageLabel As Label = page.FindControl("MessageLabel") 'finds the label
        Assert.AreEqual("Hi John!", MessageLabel.Text, "Invalid text") 'check text
End Sub
 
ASP.NET Code Example C#
public void Test()
{
   var session = new TestSession(); /start a browsing session
   var page = session.GetPage("Default.aspx"); /request a page
   var NameTextBox = (TextBox)page.FindControl("NameTextBox"); /find the textbox
   NameTextBox.Text = "John"; /enter the text
   page = session.ProcessPostback("SayButton"); /click the button*/
   var MessageLabel = (Label)page.FindControl("MessageLabel"); /finds the  label
   Assert.AreEqual("Hi John!", MessageLabel.Text, "Invalid text!"); /checks the  text
}
 
Although this looks similar to a typical Windows Forms test, there's something really unusual going on. The first request happens on line 6 (see the VB version), but after it's finished, the Page instance is still alive. Not only we can inspect its properties and controls, we can even set some properties and post the values back, “clicking” a button or a similar control. This is possible because the “server” and the “client” code live in the same process.
But wait, there's more. As you will see in the next section, not only you can execute test code before or after a request (or, as in our first test, between the requests). You can execute it duringthe request, attached to one of the page's lifecycle events.
Back to our test, in order to make it pass, you just handle the button click event:
 
protected void SayButton_Click(object sender, EventArgs e) {
 this.MessageLabel.Text = String.Format("Hi {0}!", this.NameTextBox.Text);
}
But then, you might discover that this code is a mix of presentation logic and view concerns, and might want to refactor it into View and Presenter classes. We'll follow the classic MVP pattern, only without the Model part. The View doesn't hold a reference to the Presenter, but rather raises the appropriate events, to which the Presenter is subscribed. The Presenter then invokes the appropriate method on the View, modifying the output. For example, the View raises the SayHi event in response to the button click, and has the ShowMessage method that modifies the text of the label.
Following the tradition, we'll define the IView interface as follows:
public interface IView
{
    event EventHandler SayHi;
    string Name { get; }
    void ShowMessage(string message);
}
 
Testing the View events
Our next task is to verify that our View raises the SayHi event in response to the button click.
There are a number of steps we should perform in order to verify that the SayHi event is raised. We should subscribe to this event somehow, which means we should have a reference to its source, the page. This should happen after the page is created, but before the postback event is raised, so we have to execute this code during the request. We can achieve our goal adding a handler to the page's Init event. In other words, since we can't just subscribe to the SayHi eventbefore the postback (the sender doesn't exist yet), we have to do it during the postback, and the best way is to handle the Init (or Load) event, which Ivonna can help you with. This becomes a two-step process:
1.    Subscribe to the Init event using Ivonna's infrastructure.
2.    In the Init event handler, subscribe to the SayHi event. The page instance is the sender parameter of the Init handler, and we should cast it to IView.
While the IView interface is defined in the App_Code folder, we can use it if we reference the App_Code assembly in the precompiled Web's bin folder. Typically, you would want to put it into a separate assembly.
Let's see the test code:
Code Sample ASP.NET - VB.NET
Public Class EventTester
     Dim catcher As New Ivonna.Catchers.EventCatcher
     Public Sub WhenButtonIsClicked_SayHiEventIsRaised()
     Dim session As New TestSession
     'Create a WebRequest instance corresponding
      'to a postback initiated by clicking the SayButton button.
      Dim request = New WebRequest("Default.aspx", "POST", "SayButton", Nothing)
     'Attach the catcher to the event
      request.EventHandlers.Page_Init = AddressOf AttachTheCatcher
      session.ProcessRequest(request)
      Assert.IsTrue(catcher.Raised, "Should have raised the event")
      End Sub
 
 Sub AttachTheCatcher(ByVal view As IView, ByVal e As EventArgs)
     AddHandler view.SayHi, AddressOf catcher.CatchEvent
      End Sub
End Class
 
Code Sample ASP.NET - C#
public class EventTester
{
    [Test]
    public void WhenButtonIsClicked_SayHiEventIsRaised()
    {
        /start a browsing session
        var session = new TestSession();
        var catcher = new Ivonna.Catchers.EventCatcher();
        /Create a WebRequest instance corresponding
        /to a postback initiated by clicking the SayButton button.
        var request = new WebRequest("Default.aspx", "POST", "SayButton", null);
        request.EventHandlers.Page_Init = delegate(object sender, EventArgs e)
        {
            var view = sender as IView;
            /Attach the catcher to the event; when the event is raised,
            /the catcher will record this for further verification
            view.SayHi += catcher.CatchEvent;
        };
        session.ProcessRequest(request);
        Assert.IsTrue(catcher.Raised, "The event hasn't been raised");
    }
}
 
First, we create a TestSession instance, as we do in almost every Ivonna test. Then, we have to retrieve the page we are going to post. At line 13 (C# version), we create an instance of EventCather. This is a helper class designed to verify event raising. Next, we create a WebRequest instance. This is different from our previous test, where we used session.GetPage(), but this syntax allows us to modify the request before processing. At line 17, we add a delegate to the page's Init event (VB doesn't support anonymous delegates, so we have to create a separate Sub). Note that we can do it before the page itself is created: the delegate is stored in another object and attached after the page is created. The delegate casts the page to IView and attaches another delegate to the SayHi event. After we have prepared everything, we execute the postback at line 23 and verify that the SayHi event has been raised at line 24.
In order to make the test pass, we have to add two modifications to out page. We have to implement IView, since the test raises an InvalidCastException exception at line 18. And we have to raise the SayHi event in the SayButton_Click method. But making the test pass is beyond the scope of this article.
Testing the View methods
In response to events raised by the View, the Presenter invokes various methods that change the View's appearance. In our case, the Presenter should instruct the View to display the message. It is up to the View to decide how the message is displayed. We already know that the message should appear as the text of MessageLabel. So, our purpose is to verify that invoking the ShowMessage method changes the MessageLabel's text appropriately.
This method is invoked during the page's life cycle, and is better tested using the Prerender even handler, similar to the previous test. However, we can simplify things a little and invoke the method after the request, which is executed in order to obtain the page object:
Code Sample ASP.NET - VB.NET
Public Class MethodTester
     Public Sub WhenShowMessageIsInvoked_MessageLabelShowsTheMessage()
      Dim session As New TestSession 'start a browsing session
      Dim page = session.GetPage("Default.aspx") 'request a page
      DirectCast(page, IView).ShowMessage("message") 'invoke the method
      Dim messageLabel As Label = page.FindControl("MessageLabel")'find the label
      Assert.AreEqual("message", messageLabel.Text, "Invalid text") 'checks the text
     End Sub
End Class
 
ASP.NET Code Sample - C#
public class MethodTester
{
  [Test]
  public void WhenShowMessageIsInvoked_MessageLabelShowsTheMessage()
  {
    var session = new TestSession(); /start a browsing session
       var page = session.GetPage("Default.aspx"); /request a page
       (page as IView).ShowMessage("message"); /invoke the method
 
       Label messageLabel = page.FindControl("MessageLabel") as Label;/finds the label
       Assert.AreEqual("message", messageLabel.Text, "Invalid text");/checks the text
  }
}
 
Conclusion
Ivonna makes possible to write both integration and unit tests. In fact, combined with the power of TypeMock Isolator, you can write functional tests for any part of your application: mock the external dependencies to test your system, mock the repositories to test the MVP part, etc. Developers are not forced to move all code from the page being tested to an external library; instead, you are free to start with integration tests and refactor gradually, or start with unit testing the page and add integration tests later.

Write your comment now