Unit Testing ASP.NET User Controls
 
Published: 16 Aug 2007
Abstract
This article demonstrates some of the challenges with ASP.NET user control customization, and how they can be tested in ASP.NET.
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 30317/ 51

Introduction

I wrote in a past article about unit testing ASP.NET pages. It is also possible to work with user controls, with some additional considerations.  User controls inherit from System.Web.UI.UserControl class; however, it is possible to create an intermediate class, between the UserControl class and the ASP.NET user control (.ascx). This can perform more advanced capabilities and allow the user control to be unit tested, at least partially.

In addition, by crafting an application in a certain way, it can be possible to perform more advanced testing, such as the testing of dynamic table generation within the user control, of data binding to a data-bound control, or other features.

Test Setup

There are two ways to setup a unit test for a user control. The first is to have the unit test inherit from the target, as illustrated below. Note that the user control base class (AssignmentUserControlBase) is defined as an abstract class.

Listing 1

[TestFixture]
Public void AssignmentUserControlBaseTest : AssignmentUserControlBase { }

Each user control test implements the abstract properties/methods of the base class. Essentially, this custom base class sits between the System.Web.UI.UserControl class and the ASCX code-behind page. The benefit to this is that the AssignmentUserControlBase class exists in a class library, compiled as a DLL, which can be used in unit testing.

The alternative approach is to create a unit test that instantiates an instance of the user control.  I am not going to use this approach because it makes it harder to unit test the user control.  Some of the benefits will be discussed later.

Initial Assignments

ASP.NET events in the page lifecycle can be manually triggered. When testing a single ASP.NET user control, each of the event methods can be called to simulate an ASP.NET user control's lifecycle. For instance, to call these methods the following initialization sets up the page lifecycle.  All that is needed is a call to the associated On<Event> method because these methods eventually call the appropriate event, as defined in the Control class. That is one of the beauties of using inheritance in this situation; it allows you to have direct access to these methods. Note that reflection could be used in a non-inheritance scenario.

Listing 2

[TestFixtureSetUp]
Public void Initialize()
{
  this.Page = new AssignmentPageTest();
  this.OnInit(EventArgs.Empty);
  this.OnLoad(EventArgs.Empty);
  this.OnPreRender(EventArgs.Empty);
}

In ASP.NET the Page property is assigned upon creating the user control in the page lifecycle. In a unit-testing environment, this assignment is not automatic, so a class that inherits from System.Web.UI.Page is passed to the property. But the question is, why the AssignmentPageTest class?

Each user control is usually a specialized section of a target page. This approach is usually more coupled than general ASP.NET user controls individually because the custom user control class is directly coupled with the ASCX user control. That is not necessarily true in all situations though, but is true more often than not.

Because this control would normally be a part of the AssignmentPage custom page class, it seems most fitting to use that. However, AssignmentPage most likely is a defined abstract, while AssignmentPageTest is a concrete implementation of the class that the unit test knows about.

Therefore, an instance of the concrete implementation of AssignmentPage is created and passed to the Page property. This ensures the user control can use Page properties, like IsPostback, within the code without an exception being thrown (because the Page property would be null otherwise).

When used in the context of a custom page unit test (such as AssignmentPageTest), the page must invoke its methods. Then, each subsequent user control must invoke its own methods as well; these are not automatically called for you. Though not directly accessible in the page unit test setting, reflection can be used to invoke the OnInit, OnLoad, and OnPreRender methods (as well as any other relevant methods you use).

Other Testing Capabilities

Through the unit test inheriting from the custom user control, there are some additional capabilities you get from doing so, such as implement the observer pattern. The user control class can setup or change the property values of various inner controls.  For instance, other controls can be exposed through public properties of the user control base class, and the various properties can be set, as shown below.

Listing 3

public abstract class AssignmentUserControlBase
{
  protected abstract DetailsView AssignmentDetailsView
  {
    get;
  }
  protected abstract GridView AssignmentsView
  {
    get;
  }
  protected abstract TextBox SearchTextBox
  {
    get;
  }
 
  protected override void OnInit(EventArgs e)
  {
    this.AssignmentsView.DataKeyNames = new string[]
    {
      "AssignmentID"
    };
    this.AssignmentsDetailsView.Visible = (this.AssignmentsView.SelectedIndex >
      - 1);
    this.SearchTextBox.Enabled = (this.AssignmentsView.SelectedIndex ==  - 1);
  }
}

As another example, I had an application that allowed access to a registration page only at certain times. The application only allowed registrations before Wednesday afternoon.  After that time, registration was shut down and the registration began again on the next day on Thursday, registering users for the following week.

The approach I used was to create a method that returned the current date, and was defined as protected virtually. In all situations in the real application, this method returned DateTime.Now; however, using this approach allowed me to mock the return value by overriding the method in the unit test and returning the value of a local variable. The overridden method in the unit test looks like this:

Listing 3

private DateTime _currentDate = DateTime.Now;
protected overrides DateTime GetCurrentDate()
{
  return _currentDate;
}

The method returns the current date to the OnInit method in the custom user control class, which controls the times when the application is accessible. The following is that method definition.

Listing 4

protected overrides void OnInit(EventArgs e)
{
  DateTime currentDate = this.GetCurrentDate();
  if (currentDate.DayOfWeek == Wednesday)
  {
    if (currentDate.Hour >= 15)
      //Avoid registration page
    else
      //Allow registration, display warning
  }
      //Else allow access to application
}

The following unit test can test this logic thoroughly because it overrides the GetCurrentDate method and returns the local date variable with a predefined value, which tests the various flows through the OnInit method. The following test asserts that all of the appropriate conditions do what they are meant to do. In instances when the current time is passed the fifteenth hour (past 3 PM), the page should redirect. If it is before that, a warning should be displayed; otherwise, general page access is allowed. The following test can test all of that.

Listing 5

[Test]
public void TestInitialLoad()
{
  _currentDate = new DateTime(2007, 7, 11, 9, 0, 0);
  this.OnInit(EventArgs.Empty);
  //assert that the registration is allowed, and a warning is displayed
 
  _currentDate = new DateTime(2007, 7, 11, 16, 0, 0);
  this.OnInit(EventArgs.Empty);
  //assert that the registration is blocked
 
  _currentDate = new DateTime(2007, 7, 12, 7, 0, 0);
  this.OnInit(EventArgs.Empty);
  //assert that the registration is allowed for the next week
}

If a user control dynamically generates the controls within it, it is possible to test that as well.  For instance, assume that a server-side table is dynamically generated on page load, adding certain controls within certain cells to control the layout. The text within those controls may be dynamically generated from data in this situation. Everything in regards to that process can be tested to ensure the table renders correctly. Take the test below. The first part defines the abstract property that must be overridden (which allows the custom user control class to make use of the visual elements in the ASCX user control). Then, the table's structure is validated after it is initialized (initialization is called in the test's setup method).

Listing 6

private Table _layoutTable = new Table();
protected override Table LayoutTable
{
  get
  {
    return _layoutTable;
  }
}
 
[Test]
public void TestTableGeneration()
{
  Assert.AreEqual(3, _layoutTable.Rows.Count);
  Assert.IsInstanceOfType(typeof(Label),
    _layoutTable.Rows[0].Cells[0].Controls[0]);
  Assert.AreEqual("Search Text:", ((Label)
    _layoutTable.Rows[0].Cells[0].Controls[0]).Text);
 
  Assert.AreEqual(2, _layoutTable.Rows[1].Cells[0].ColumnSpan);
  Assert.IsInstanceOfType(typeof(GridView),
    _layoutTable.Rows[1].Cells[0].Controls[0]);
 
  Assert.AreEqual("5 rows found", _layoutTable.Rows[2].Cells[0].Text);
}

This makes it possible to use Test-Driven Development (TDD) to formulate the structure of the user control and to test the layout creation of the user control.

Conclusion

It is possible to perform some of the bulk user control code in the custom user control class. This class is often a one-to-one relationship to the ASP.NET user control, meaning that it is often specialized for a single user control, but is not necessarily always that way. It is possible to perform more in-depth tests and test the functionality more deeply using some of the approaches discussed above.



User Comments

Title: mr   
Name: rob
Date: 2012-03-15 4:55:09 AM
Comment:
would love a complete solution with usercontrol project and test projects for reference.

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-29 1:03:38 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search