Testing ASP.NET WebForms

ASP.NET WebForms

ASP.NET WebForms was released as part of the .NET platform in 2002. The release was a major milestone for developers all over the world, allowing them to produce more powerful web applications using the new C# language and Visual Studio.NET. One of the key advantages to ASP.NET was being able to quickly develop applications by taking advantage of built-in controls that allowed you to quickly display, edit, and delete data from a database and, in later versions, complete membership systems with the hard work already done for you.

 

The result was developers being able to drag and drop controls onto a page and then release the site. It allowed for a very quick turn around, but at no point was architecture of the application or testability considered. Developers found that as their requirements grew and they needed to move beyond the built-in controls they faced major problems. One of these problems was testability and being unable to effectively test ASP.NET applications. While this is still partly true, with some consideration you can unit test ASP.NET WebForms applications.

Unit Testing ASP.NET WebForms

An ASP.NET application is made up of two main parts. The aspx file contains the ASP.NET markup, which is a mixture of HTML, ASP.NET controls, and ASPX tags that contain code to be executed on the server before being returned to the user. The second part is the aspx.cs file that is the code-behind for a particular page. The code-behind files contain C# code to support the main ASP.NET page, allowing you to hook into the page lifecycle and execute code on the server when the page loads, or more commonly when a user requests some data that needs to be processed by the server.

However, because these code-behind files hook into the lifecycle of the page they have a tight dependency to the core ASP.net runtime. When it comes to testing, this tight dependency causes a huge amount of problems. If you attempt to access the object outside of the ASP.net runtime, such as when writing unit tests then you will receive a number of error messages in and around the core engine. When it comes to the aspx file, any of the code placed in the view is not accessible from another class – as such you wouldn’t be able to write any unit tests for it.

In order to be able to unit test ASP.NET WebForms you need to think about how to develop the application in a different way. The most important thing to remember is to keep the view and the code-behind classes as thin as possible. They should contain the minimum amount of code to function, and as soon as the code starts to become complex it should be moved to a separate isolated class. We feel that the way Visual Studio and ASP.NET WebForms behave by default encourages you to store a large amount of logic within your code-behind file; however, this is not the correct place for complex logic. If you think back to Chapter 2 and SOLID, The Single Responsibility Principle is broken by code-behind files.

ASP.NET also doesn’t help to keep the view lightweight as it encourages controls such as the GridView control. An example of the GridView is next; the control populates the grid and provides built in paging support:

        <asp:GridView ID="GridView1" runat="server" AllowPaging="True"

            AutoGenerateColumns="False" DataSourceID="ObjectDataSource1">

            <Columns>

                <asp:CommandField ShowSelectButton="True" />

                <asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />

            </Columns>

        </asp:GridView>

        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server"

            SelectMethod="GetData" TypeName="WebApplication1.ObjectSourceBusinessObject">

        </asp:ObjectDataSource>

This kind of code litters your aspx files while also sending over-complex HTML down to the user. This doesn’t mean that it needs testing as it’s a standard control, but it does mean that there is not a clean separation and no possible way to write unit tests. Even automated UI tests would be difficult.

One way to help encourage you to keep your view and code-behind files clean is to follow the Model-View-Presenter (MVP) design pattern. A design pattern describes some experience or knowledge gained allowing this to be shared with other people. The idea is to allow people to take one person’s experience, use the pattern to understand the concept in a high level, and then apply the techniques to their own scenario. Having a pattern in this high-level format allows you to discuss the concepts in the same way, leaving low-level implementation details out of the discussion. The advantage is that the discussion can focus on the technical aspects, without focusing on someone’s specific problems. There are many different patterns, and MVP is just one of them.

When it comes to ASP.NET and MVP, the aim is to move all of the normal logic you would find in a code-behind file into a separate class. In order for this class to communicate with the view, the view implements a particular interface defining the view’s behavior. The view is then injected into the controller so that it can take advantage of the functionality the view offers. In this example, the view will have two elements, one element is a button and the other is a ListBox. When the user clicks the button, it will request data from the server and populate the result in the ListBox.

If you look at the implementation, you’ll see that the view will need to implement an interface. In this example, you need the view to be able to fire an event in order to request data, have a property for the data to be stored and a method to cause the UI to update and display the data:

    public interface IDisplayDataView

    {

        event EventHandler DataRequested;

        List<string> Data { get; set; }

        void Bind();

    }

After the view has implemented the interface, it can pass the instance of itself into a controller, in this case the DisplayDataController:

    public partial class _Default : System.Web.UI.Page, IDisplayDataView

    {

        private DisplayDataController Controller;

        protected void Page_Load(object sender, EventArgs e)

        {

            Controller = new DisplayDataController(this);

        }

    }

The controller stores the view instance for future methods calls. Within the constructor, the controller also needs to hook up any events provided by the UI and attach them to the appropriate method to ensure that the functionality is met:

    public class DisplayDataController

    {

        public IDisplayDataView View { get; set; }

 

        public DisplayDataController(IDisplayDataView view)

        {

            View = view;

            View.DataRequested += GetData;

        }

     }

Because this code is in an isolated class away from the ASP.NET lifecycle, you can write a test to verify that the functionality is correct. In this case, you are verifying that an event handler is attached to the DataRequested event, but you don’t care which type of handler. After constructing the controller, you verify that this has happened:

    [TestFixture]

    public class DisplayDataControllerTests

    {

        [Test]

        public void Ctor_should_hook_up_events()

        {

            IDisplayDataView view = MockRepository.GenerateMock<IDisplayDataView>();

            view.Expect(v => v.DataRequested += null).IgnoreArguments();

 

            new DisplayDataController(view);

 

            view.VerifyAllExpectations();

        }

     }

With the event in place, the next step is to implement the GetData method, which will be called when the event is raised. Again, as you have abstracted away from the ASP.NET code-behind file, you can write the code test-first. The idea is that when GetData is called, it should populate the Data property on the view. To simulate the view, you can use Rhino Mocks to produce a stub object. The controller will then interact with the stub and the Data property as if it was a real page. By having the IDisplayDataView in place you have the control and flexibility to be able to test. If the controller does not populate the data property after a call to GetData, then the test will fail:

       [Test]

        public void GetData_should_populate_data()

        {

            IDisplayDataView view = MockRepository.GenerateStub<IDisplayDataView>();

            DisplayDataController controller = new DisplayDataController(view);

            controller.GetData(this, EventArgs.Empty);

 

            Assert.AreEqual(4, view.Data.Count);

        }

After the data has been set, the view needs to be alerted in order to refresh the UI and display the data to the user. There are many ways this notification could be made, in this example you expect that the controller will call the Bind method on the view.

In our controller test, you can use a mock object to verify that the call is made correctly after GetData is called:

        [Test]

        public void GetData_should_call_Bind()

        {

            IDisplayDataView view = MockRepository.GenerateMock<IDisplayDataView>();

            view.Expect(v => v.Bind());

            DisplayDataController controller = new DisplayDataController(view);

            controller.GetData(this, EventArgs.Empty);

 

            view.VerifyAllExpectations();

        }

The results of the implementation via the tests would be next:

        public void GetData(object sender, EventArgs e)

        {

            View.Data = new List<string> { "String A", "String B", "String C", "String D" };

            View.Bind();

        }

While MVP is a useful pattern to consider when developing WebForms applications and can definitively improve testability, it’s still not perfect. The problem is that the ASP.net application still needs all the events to go via the code-behind file. So, you still have methods containing logic that is unable to be tested. In this example, there are the following two methods that are untested as a result:

        public void Bind()

        {

            ListBox1.DataSource = Data;

            ListBox1.DataBind();

        }

        protected void Button1_Click(object sender, EventArgs e)

        {

            if (DataRequested != null)

                DataRequested(sender, e);

        }

While two methods won’t cause major problems, you can imagine that as the application grows, this untested code will also increase, which might result in more bugs being introduced into the code base. For example, when writing this example you had 100% percent passing tests, but because you didn’t call ListBox1.DataBind(), nothing was displayed on the UI.

MVP also doesn’t provide much support when attempting to move code from the view into testable classes. While it’s possible, it can increase the complexity of the application.

The use of MVP pattern was never a mainstream approach to developing ASP.NET WebForms. People wrote code in the code-behind file and also wanted to be able to unit test it. This became a major issue for Microsoft with developers starting to get frustrated with the lack of testability when compared with other open source frameworks. These open source frameworks offered huge advantages to developers over WebForms, with many people taking advantage and using these frameworks. As a result, Microsoft released a new framework based on the MVC pattern called ASP.NET MVC. This was a huge jump forward in terms of testability support as it solves some of the issues we mentioned with WebForms and the MVP pattern.  

This article is excerpted from chapter 3 "Unit Testing and Test Driven Development" of the book Testing ASP.NET Web Applications by by Jeff McWherter and Ben Hall (ISBN: 978-0-470-49664-0, Wrox, 2009, Copyright Wiley Publishing Inc.)

Tags:

Comments

One response to “Testing ASP.NET WebForms”

  1. Anonymous says:

    Question: 
    Don’t you need to implement all the properties and methods mentioned in the interface IDisplayDataView , in your code behind ? I tried to use the same in ASP.NET Web Forms using MVP pattern, but I am cannot proceed when I don’t  include the methods and properties that are in the interface. Is there anything that is missing in the article?

Leave a Reply

Your email address will not be published. Required fields are marked *