Testing your ASP.NET control (part 1 of hopefully many): ViewState

A typical ASP.NET server control will store at least some of its properties in ViewState. For example, the Label control saves the value of its Text property in ViewState so that on following postbacks the value does not need to be explicitly set again. In the first part of this series (which I hope will be extensive) we'll see how to write general unit tests for a control, and then write a unit test that ensures a property is being saved in ViewState.

In order to demonstrate this I wrote a simple label control:

namespace Eilon.Sample.Controls {
    using System;
    using System.Web.UI;

    public class NewLabel : Control {
        public string Text {
            get {
                string s = ViewState["Text"] as string;
                return s ?? String.Empty;
            }
            set {
                ViewState["Text"] = value;
            }
        }

        protected override void Render(HtmlTextWriter writer) {
            writer.Write(Text);
        }
    }
}

It has a single public property, "Text," and it overrides the Render method to render the value of that property. Let's start out by writing some simple unit tests to ensure our property and Render method work. I'm using the attributes that come the Visual Studio's unit testing libraries since work in VSTS as well as in NUnit (and for those who are wondering, I'm using NUnit!).

namespace Eilon.Sample.Controls.Test {
    using System;
    using System.IO;
    using System.Web.UI;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class NewLabelTest {
        [TestMethod]
        public void TextReturnsEmptyStringDefault() {
            NewLabel label = new NewLabel();
            Assert.AreEqual<string>(String.Empty, label.Text,
"Default text should be empty string (not null)"); } [TestMethod] public void GetSetText() { const string value = "Some Text"; NewLabel label = new NewLabel(); label.Text = value; Assert.AreEqual<string>(value, label.Text,
"Property value isn't the same as what we set"); } [TestMethod] public void RenderEmpty() { NewLabel label = new NewLabel(); Assert.AreEqual<string>(String.Empty, GetRenderedText(label),
"Shouldn't have rendered anything"); } [TestMethod] public void RenderWithText() { const string value = "Some Text"; NewLabel label = new NewLabel(); label.Text = value; Assert.AreEqual<string>(value, GetRenderedText(label),
"Should have rendered the text"); } private static string GetRenderedText(Control c) { HtmlTextWriter writer = new HtmlTextWriter(new StringWriter()); c.RenderControl(writer); return writer.InnerWriter.ToString(); } } }

So far it looks like we're getting pretty good code coverage. Although I didn't measure, I'm pretty sure we're getting 100% code coverage. That means we're done, right? Wrong! An important aspect of the design of this control is that we specifically wanted to save the value of the Text property in ViewState so that it could be restored on a subsequent postback. We have tests to make sure that the get/set accessors work, but nothing specifically about ViewState.

The first problem we encounter with ViewState is that all the methods associated with it are protected and thus cannot be accessed from our unit test code directly. There are several ways to overcome this; here are a few:

  1. Use reflection from our unit test to call the methods. While this technique is popular and fairly easy to implement, in some cases it's hard to get the method signatures right (especially with out and ref parameters), and you lose compile time checking at some stages of the execution.
  2. Create an internal interface such as IViewStateProvider (it would appear almost identical to IStateManager), which at runtime uses Control's ViewState implementation. I typically use this approach when I need to create mocks as opposed to just allowing me to access certain members.
  3. Expose Control's ViewState methods though an internal interface.

Here we'll use technique #3 since it fits my needs perfectly. The first step is to create the internal interface with all the methods we need:

    // Interface to expose protected methods from
    // the Control class to our unit test
    internal interface IControl {
        void LoadViewState(object savedState);
        object SaveViewState();
        void TrackViewState();
    }

And then we implement it on our NewLabel class:

        #region IControl Members
        void IControl.LoadViewState(object savedState) {
            LoadViewState(savedState);
        }

        object IControl.SaveViewState() {
            return SaveViewState();
        }

        void IControl.TrackViewState() {
            TrackViewState();
        }
        #endregion

And the last step to expose it to our unit test assembly is to use the InternalsVisibleTo attribute so that the unit tests can call our internal interface:

using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyControlLibrary.Test")]

Now that we have the infrastructure set up, we need to write a unit test that tests the ViewState functionality:

        [TestMethod]
        public void TextSavedInViewState() {
            // Create the control, start tracking viewstate,
            // then set a new Text value
            const string firstValue = "Some Text";
            const string secondValue = "ViewState Text";
            NewLabel label = new NewLabel();
            label.Text = firstValue;
            ((IControl)label).TrackViewState();
            label.Text = secondValue;

            // Save the control's state
            object viewState = ((IControl)label).SaveViewState();

            // Create a new control instance and load the state
            // back into it, overriding any existing values
            NewLabel newLabel = new NewLabel();
            label.Text = firstValue;
            ((IControl)newLabel).LoadViewState(viewState);

            Assert.AreEqual<string>(secondValue, newLabel.Text,
                "Value restored from viewstate does not match the original value we set");
        }

While we didn't increase code coverage (we were already at 100%!), we've now made sure we had a unit test to cover every scenario in the specification for the control. We only wrote unit tests for code that we wrote, including things that weren't immediately obvious.

Download the entire project (control library, sample site, and unit test library).

And I do really use NUnit:

- Eilon 

3 Comments

Comments have been disabled for this content.