AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=1328&pId=-1
ASP.NET OOP and Unit Testing
page
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 38818/ 55

Introduction

ASP.NET is a very functional framework for developing web applications. Creating an object-oriented solution is harder to do so because of the coding effort involved with doing so. This coding effort means creating a custom page or user control class that drives the ASP.NET page.

You may wonder why you would want to do all that work to get around a few complicated problems. The biggest benefit that I have found is that with this extra work, it is possible to unit test ASP.NET pages, which is a huge benefit to ensure that application logic is correct. You can ensure that controls are shown/hidden, that the data displays correctly, and test many of the other visual mechanisms as well. The drawbacks to this approach are the strong coupling between the ASP.NET page and the custom page object and the level of coding effort involved.

There are some challenges when working with ASP.NET objects that I will explain in detail later on. Before we start looking at the problem and the solution, an understanding of some of the capabilities is needed.

Custom ASP.NET Classes

Each ASP.NET page inherits from System.Web.UI.Page; however, a custom page class can inherit from this System.Web.UI.Page, and all ASP.NET pages can inherit from this custom page. This custom page class is a great feature because it can provide generic or specialized properties/methods that are useful to an application or a specific page. User controls work the same way; a custom UserControl class can be used, which can expose properties/methods to all of the user controls in a web application. Throughout this article the custom page class and custom user control class that serves as an intermediary will be referred to as "the custom page class" or "the user control class."

Unit testing has several complications when testing against an ASP.NET page. First, this approach usually involves the unit test inheriting from the ASP.NET page. In addition, ASP.NET utilizes a different set of runtime objects (Handlers/Modules) that are available in the web environment and not in the unit testing environment. For instance, the session object makes use of a module that is not available normally in the test environment. Creating a custom session factory for testing purposes only can be overly complicated and not worth the effort it takes to develop one for unit testing purposes. In addition, the request/response collection is also specialized to the web environment, and not available in the unit testing environment. Caching is not the same, because one can make use of the Enterprise Library Caching block, which works in the windows realm as well.

The ASP.NET default model can also provide additional complications. Within a custom page or user control class, ASP.NET controls can be exposed, but simple and complex properties, templates, and other server-control settings may not be directly accessible in code-behind. In addition, what happens when a custom user control base class is reused between multiple user controls? The markup code has to be duplicated. For instance, take the markup below.

Listing 1

<asp:GridView id="gvwProducts" runat="server" AutoGenerateColumns="false"
      DataSourceID="sdsProducts">
      <Columns>
            <asp:BoundField HeaderText="ID#" DataField="ProductID" />
            <asp:BoundField HeaderText="Name" DataField="ProductName" />
            <asp:BoundField HeaderText="Cost" DataField="Cost" />
      </Columns>
</asp:GridView>

The GridView control has three columns, all in the markup.  However, if the page or user control is considered reusable, a better implementation is to move the setup of the GridView to the page or user control class, and create them at runtime.

There are good reasons to do this: creating them in the custom page or user control class increases reusability, increases how much the page/user control class knows about the server controls, and increases how much can be manipulated. The drawbacks are, as I stated before, extra coding effort.

Session/ Cache Objects

I mentioned before the complications with some of the ASP.NET collections objects, like Request, Response, Cache, Session, and ViewState. These objects are specialized and to customize the actual implementations can be complicated and can take some work.  How can we expose this?  Let us look at the Session collection. Session is a specialized collection and to use the Session at a class level is available through the Page object as well as the HttpContext.Current property.

However, in a test environment, only a dictionary of string keys and object values is needed, and because it is the test environment, the test does not have as many problems as the web environment. Since the test exists outside of any page lifecycle and will not be recycled at the unloading of the page, the unit test does not need to worry about serializing and storing the values.  This is both good and bad.

But, what if abstraction was added? What if a custom class representing both the ASP.NET caching mechanism and a testing mechanism can be used instead?  For instance, below could be a session state manager interface.

Listing 2

public interface ISessionStateManager
{
  int Count
  {
    get;
  }
  object this[string key]
  {
    get;
    set;
  }
  string UniqueKey
  {
    get;
  }
  void Add(string key, object value);
  void Clear();
  bool Contains(string key);
  void Remove(string key);
  void RemoveAt(int index);
}

Any class implementing this interface could use a custom session state management, using any mechanism available.  For instance, one class can utilize the ASP.NET session manager and one could implement a test version using an internal dictionary that stores key/value pairs.

Remember that I mentioned the Session could be accessed through HttpContext.Current.  The ASPNetSessionManager implements the session interface as below.  It uses the Session object as the sole point of contact with the actual session object, which will be shown later.

Listing 3

public class AspNetSessionStateManager: ISessionStateManager
{
  public int Count
  {
    get
    {
      return Session.Count;
    }
  }
  public object this[string key]
  {
    get
    {
      return Session[key];
    }
    set
    {
      Session[key] = value;
    }
  }
 
  public string UniqueKey
  {
    get
    {
      return Session.SessionID;
    }
  }
 
  public void Add(string key, object value)
  {
    Session.Add(key, value);
  }
 
  public void Clear()
  {
    Session.Clear();
  }
 
  public bool Contains(string key)
  {
    return (Session["key"] != null);
  }
 
  public void Remove(string key)
  {
    Session.Remove(key);
  }
 
  public void RemoveAt(int index)
  {
    Session.RemoveAt(index);
  }
}

The Session property is private and local to this instance (not exposed by the ISessionStateManager interface) and returns an instance of the HttpSessionState, returning the ASP.NET session object when in the ASP.NET environment.

Listing 4

private System.Web.SessionState.HttpSessionState Session
{
  get
  {
    if (HttpContext.Current == null)
      throw new Exception("The context is currently null");
    return HttpContext.Current.Session;
  }
}

For testing purposes, I create a stub that stores the session keys in an internal dictionary. Any custom page class using my session interface, instead of the session object directly (which is still available) can be safely tested, as long as the correct session manager is configured (mentioned below). That is safe for unit testing and is only meant for unit testing because the stub does not serialize and store the session values across ASP.NET postbacks.

Listing 5

public class SessionStateManagerStub: ISessionStateManager
{
  private Dictionary < stringobject > _session = new Dictionary < string,
    object > ();
 
 
  public int Count
  {
    get
    {
      return _session.Count;
    }
  }
 
  public object this[string key]
  {
    get
    {
      return _session[key];
    }
    set
    {
      _session[key] = value;
    }
  }
 
  public string UniqueKey
  {
    get
    {
      return "1234567890";
    }
  }
 
  public void Add(string key, object value)
  {
    _session.Add(key, value);
  }
 
  public void Clear()
  {
    _session.Clear();
  }
 
  public bool Contains(string key)
  {
    return _session.ContainsKey(key);
  }
 
  public void Remove(string key)
  {
    _session.Remove(key);
  }
 
  public void RemoveAt(int index)
  {
    int i = 0;
    if (index < 0 || index >= _session.Count)
      throw new ArgumentOutOfRangeException("index");
 
    foreach (KeyValuePair < stringobject > pair in _session)
    {
      if (index == i++)
      {
        this.Remove(pair.Key);
        return ;
      }
    }
 
    throw new KeyNotFoundException("The index was not found");
  }
}

The problem with using this test version is that the ASP.NET version never gets tested correctly, at least with unit testing alone. There are other tools available, such as Plasma or WATIN that can unit test ASP.NET pages by accessing it (using HTTP) and parsing the HTML.

Both implementations work great, but how can you switch between the two?  This is where the configuration capabilities come into play.  ASP.NET provides the ability to create custom configuration sections, as well as specify the provider type as a string property within the custom section. Because the provider type is a string, the type can be dynamically created through the Type.GetType() method.

How does this dynamic session manager get exposed? Through a SessionStateManagement static object, similar to how the provider pattern works. It exposes all of the methods defined in ISessionStateManager, plus provides additional properties. For instance, one of the properties is the Enabled property defined in the configuration section, as shown below.

Listing 6

public static bool Enabled
{
  get
  {
    if (SessionStateSettingsSection.Instance == null)
      return false;
    else
      return SessionStateSettingsSection.Instance.Enabled;
  }
}

If disabled, an exception will be thrown disallowing any class to make use of the session capabilities. SessionStateSettingsSection also defines the session provider type, which will be used in the call to GetManager(). To expose the manager selected, the Manager property handles this.

Listing 7

private static ISessionStateManager Manager
{
  get
  {
    if (_manager == null)
    {
      if (SessionStateSettingsSection.Instance == null)
      throw new ConfigurationErrorsException(@
        "Session statesettings section has not been initialized ");if
        (!SessionStateSettingsSection.Instance.Enabled)throw new Exception(
        "Session state is not enabled");
 
      _manager = SessionStateSettingsSection.Instance.GetManager();
    }
 
    return _manager;
  }
}

All of the methods directly call the Manager's methods.

Listing 8

public static void Add(string key, object value)
{
  Manager.Add(key, value);
}
public static void Remove(string key)
{
  Manager.Remove(key);
}

The Manager property, in turn, calls the default provider setup in the configuration section.

Listing 9

<sessionStateSettings type="Nucleo.Web.Session.SessionStateManagerStub, 
   Nucleo.TestingTools" />

And, the SessionStateManagerStub methods get called, which work with the internal dictionary to add/remove items.

Listing 10

public void Add(string key, object value)
{
  _session.Add(key, value);
}
public void Remove(string key)
{
  _session.Remove(key);
}

A few things to note, because the SessionStateManagement static class is being utilized, it requires that a configuration section be used and enabled is set to true.  If anything uses SessionStateManagement without a provider being specified in the configuration file, an exception is thrown (as you may have seen above). The dynamically referenced provider is returned otherwise.

I have created an almost identical mechanism for caching; the caching mechanism uses an ICachingManager interface, as well as a CacheManagement static object. It has an ASP.NET implementation that exposes the HttpContext.Current.Cache property, as well as a stubbed cache provider that uses an internal dictionary. But with caching, there is another custom implementation that can be used outside of the ASP.NET caching mechanism and a stubbed implementation: the Enterprise Library Caching block.  Below is an internal property that gets an instance of the EL caching manager.

Listing 11

internal CacheManager Manager
{
  get
  {
    if (_manager == null)
      _manager = CacheFactory.GetCacheManager();
    return _manager;
  }
}

The cache manager takes a key for a non-default caching manager implementation, so this implementation requires a default cache manager setup, which can pose a challenge.  Outside of this, everything works the same.

ViewState Objects

ViewState seems to work well with the unit testing framework; though I thought I remembered seeing problems with this in the past, recent tests show that it does not seem to have the problems that I thought it did. So the view state may be a safe mechanism to work with.  You can reference the keys and retrieving values from it as you would in any ASP.NET page. The internal mechanism is a System.Web.UI.StateBag object, and after investigating, I did not come across anything that may hinder unit testing.

Representing Server Controls

Representing ASP.NET server controls in a custom page or user control class can be a double-edge sword for several reasons. By adding properties or fields that represent ASP.NET server controls, this often couples the custom page class to the ASP.NET page it represents. This may not be a bad thing overall, but it will require more work to utilize its full effect. I am currently working on some code to make this implementation approach easier to work with, and that might be the subject of a future article. For now, we will be taking this approach, seen as a variant to the front controller or page controller pattern. Check out the properties below.

Listing 12

public class SignupPage : PageBase
{
  protected abstract TextBox EmailAddressBox { get; }
  protected abstract TextBox NameBox { get; }
  protected abstract RadioButtonList NewsletterSignupList { get; }
}

Instead of properties, examples using page or front controller use protected fields as you did in ASP.NET 1.1.  Because the approach that I use is not using Web Application Project (WAP), to expose controls to the custom page class requires the use of properties. These properties are overridden and return the real components for each property.

Listing 13

<%@ Page CodeFile="signup.aspx.cs" Inherits="Mains.Examples.InboxASPXPage" %>
<table>
      <tr>
            <td>Name:</td>
            <td><asp:TextBox id="txtName" runat="server" /></td>
      </tr>
      <tr>
            <td>Email:</td>
            <td><asp:TextBox id="txtEmail" runat="server" /></td>
      </tr>
      <tr>
            <td>Newsletter Signup?:</td>
            <td>
                  <asp:RadioButtonList id="rblSignup" runat="server">
                        <asp:ListItem>Yes</asp:ListItem>
                        <asp:ListItem>No</asp:ListItem>
                  </asp:RadioButtonList>
            </td>
      </tr>
</table>
public class InboxASPXPage : SignupPage
{
      protected override TextBox EmailAddressBox
      {
            get { return txtEmail; }
      }
      protected override TextBox NameBox
      {
            get { return txtName; }
      }
      protected override RadioButtonList NewsletterSignupList
      {
            get { return rblSignup; }
      }
}

This ASPX page inherits from the custom signup page above, implementing the protected properties and returning the appropriate control. The reason those properties add additional opportunities are because the custom page class can do something like this below:

Listing 14

protected override void OnLoad(EventArgs e)
{
  if (!Page.IsPostBack)
  {
    this.NewsletterSignupList.Items.Add("Yes");
    this.NewsletterSignupList.Items.Add("No");
 
    this.NameBox.Text = this.GetProfileValue < string > ("Name");
  }
}

Because the controls of the page are exposed in this fashion, the custom page class can perform whatever interaction it needs to do. Notice that the page loads the default list of items. This could be something more complex as checking with a back-end reservation system and creating a dynamic page, creating a data-driven web site, or something simpler.

In the previous sample, the ASPX markup defines a list of items; however, removing the code from that markup and adding them to the code-behind OnLoad method adds a good benefit: the list will always have a common set of known, standard values that the custom page definitely knows about.

From this, it is possible to create a unit test like the one below. The test inherits from the abstract page class and implements the server control properties with dummy controls. That is why it is nice to setup some of the server control properties in this custom code-behind page, as shown above.

Listing 15

[TestFixture]Public class SignupPageTest: SignupPage
{
  Private TextBox txtEmail = new TextBox();
  Private TextBox txtName = new TextBox();
  Private RadioButtonList rblSignup = new RadioButtonList();
 
  protected override TextBox EmailAddressBox
  {
    get
    {
      return txtEmail;
    }
  }
  protected override TextBox NameBox
  {
    get
    {
      return txtName;
    }
  }
  protected override RadioButtonList NewsletterSignupList
  {
    get
    {
      return rblSignup;
    }
  }
 
  [TestFixtureSetUp]Public void Initialize()
  {
    this.OnInit(EventArgs.Empty); //Raises init event
    this.OnLoad(EventArgs.Empty); //Raises load event
  }
 
  [Test]Public void TestInitialValues()
  {
    Assert.AreEqual(2, this.NewsletterSignupList.Items.Count);
  }
}
Setting up an Example Page Unit Test

In the example unit test I setup, I have a base class that has a series of properties.  Some of the properties are stored in viewstate, while some return an instance of controls.  For instance, below are two properties that store their base value in the viewstate.

Listing 16

protected bool IsRedirectingFromContactPage
{
  get
  {
    object o = ViewState["IsRedirectingFromContactPage"];
    return (o == null) ? false : (bool)o;
  }
  set
  {
    ViewState["IsRedirectingFromContactPage"= value;
  }
}
 
protected bool IsRedirectingFromSubmissionForm
{
  get
  {
    object o = ViewState["IsRedirectingFromSubmissionForm"];
    return (o == null) ? false : (bool)o;
  }
  set
  {
    ViewState["IsRedirectingFromSubmissionForm"= value;
  }
}

These properties get manipulated in the initial page load, through the OnLoad method.

Listing 17

protected override void OnLoad(EventArgs e)
{
  if (this.ReferringPageName.EndsWith("contact.aspx"))
    this.IsRedirectingFromContactPage = true;
  else if (this.ReferringPageName.EndsWith("submission.aspx"))
    this.IsRedirectingFromSubmissionForm = true;
}

To ensure that one of these values get set correctly, I created a ReferringPageName property in the custom page class that returns the name of the page that is referring to it.  In the unit test, it shadows the property to return a static partial url, so that one of the properties gets set to true for testing purposes.

Listing 18

new public string ReferringPageName
{
  get
  {
    return "contact.aspx";
  }
}

In addition to setting these properties, this example custom page class sets up the data key names, creates the columns, and sets up other default settings, for the GridView control.  A Timer control also exists in the page, and the OnLoad method sets up the interval.  It would be possible to change the length of the interval to longer or shorter, based on what kind of products are being showed (if the availability may be rapidly changing, or if the cost of the product is volatile, or for some other reason).

Listing 19

this.ProductsView.DataKeyNames = new string[] { "ProductID" };
this.ProductsView.Columns.Add(this.CreateColumn("Name", "ProductName"));
this.ProductsView.Columns.Add(this.CreateColumn("Cost", "Cost"));
this.ProductsView.Columns.Add(this.CreateColumn("Availability", "IsAvailable"));
this.ProductsView.AllowSorting = true;
this.ProductsView.AutoGenerateSelectButton = true;
this.Timer.Interval = 100000;

Notice the CreateColumn method; the gridview control is setup in the custom page class, instead of the ASPX markup. This prevents duplication (in both the ASPX and the unit test), but can make some of that effort harder.  The unit test that uses this base class is below.

Listing 20

[TestFixture]
public class PageBaseTest02: PageBaseTest02_Base
{
  private Label _errorLabel = new Label();
  private Label _headerLabel = new Label();
  private GridView _productsView = new GridView();
  private Timer _timer = new Timer();
 
  protected override Label ErrorLabel
  {
    get
    {
      return _errorLabel;
    }
  }
 
  protected override Label HeaderLabel
  {
    get
    {
      return _headerLabel;
    }
  }
 
  protected override GridView ProductsView
  {
    get
    {
      return _productsView;
    }
  }
 
  protected override Timer Timer
  {
    get
    {
      return _timer;
    }
  }
 
  public PageBaseTest02()
  {
    _timer.ID = "Timer1";
    _timer.Enabled = false;
  }
 
  private DataTable BindSource()
  {
    DataTable results = new DataTable();
    results.Columns.Add("ProductID", typeof(int));
    results.Columns.Add("ProductName", typeof(string));
    results.Columns.Add("Cost", typeof(decimal));
    results.Columns.Add("IsAvailable", typeof(bool));
 
    DataRow row = results.NewRow();
    row["ProductID"= 1;
    row["ProductName"= "Super Yo-Yo";
    row["Cost"= 1.95;
    row["IsAvailable"= true;
    results.Rows.Add(row);
 
    row = results.NewRow();
    row["ProductID"= 2;
    row["ProductName"= "Silly String";
    row["Cost"= 2.25;
    row["IsAvailable"= true;
    results.Rows.Add(row);
 
    row = results.NewRow();
    row["ProductID"= 3;
    row["ProductName"= "Party Whistles";
    row["Cost"= 0.95;
    row["IsAvailable"= false;
    results.Rows.Add(row);
 
    return results;
  }
 
  [TestFixtureSetUp]
  public void Initialize()
  {
    this.OnInit(EventArgs.Empty);
    this.OnLoad(EventArgs.Empty);
 
    this.ProductsView.DataSource = this.BindSource();
    this.ProductsView.DataBind();
 
    this.OnPreRender(EventArgs.Empty);
  }
 
  [Test]
  public void TestBaseProperties()
  {
    Assert.AreEqual(truethis.IsRedirectingFromContactPage);
    Assert.AreEqual(false, this.IsRedirectingFromSubmissionForm);
    Assert.IsNotNull(this.ErrorLabel);
    Assert.IsNotNull(this.HeaderLabel);
    Assert.IsNotNull(this.ProductsView);
    Assert.IsNotNull(this.Timer);
 
    Assert.AreEqual(1, this.ProductsView.DataKeyNames.Length);
    Assert.AreEqual(3, this.ProductsView.Columns.Count);
    Assert.AreEqual(3, this.ProductsView.DataKeys.Count);
    Assert.AreEqual(3, this.ProductsView.Rows.Count);
  }
 
  [Test]
  public void TestProductsViewProperties()
  {
    Assert.AreEqual(1, this.ProductsView.DataKeys[0].Value);
    Assert.AreEqual(2, this.ProductsView.DataKeys[1].Value);
    Assert.AreEqual(3, this.ProductsView.DataKeys[2].Value);
  }
}
Conclusion

Hopefully, you have seen how using a more object-oriented approach to development can make ASP.NET development more unit test friendly and can be a benefit in more complex applications that require a lot of interaction.  You can get the code from my Nucleo project at http://www.codeplex.com/nucleo.



©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-16 4:09:09 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search