Saturday, October 22, 2011

Unit testing .aspx pages


One of the inherent problems with unit testing .aspx web pages in Visual Studio is dealing with the missing HttpContext object in a Test project. There are ways that this can be mocked but this is not very intuitive nor easy to find documentation on (at least from my searching). The good news is that there is a way around this without much effort.

First off let's talk about a rudimentary solution provided by Visual Studio Test Edition that falls short of our goal of effectively unit testing an .aspx page. With this version of VS you'll see a special kind of test called a "web unit test". Oooh, sounds promising... until you actually try to use it. You launch a running web app and the test captures the all POST data from a postback. Then you can edit these POST action variables and substitute whatever values you want for them. As for your assertions you do a pattern match on the resulting html to see if a keyword you're expecting got rendered on the screen. Yeah, clunky at best.

So onward to a legitimate solution! Let's say you're writing a search screen that has several ASP server controls such as textboxes, dropdowns, radio buttons, etc. on it and below that you have a GridView to show the search results. The ideal test scenario would allow us to reference our server controls explicitly and with minimal effort. This means that in our test we'd get to set any control attribute like we normally would in code behind, we can raise control events, and lastly we can run our assertions by looking at our controls' properties.

Setting up your test project

There are two methods that you'll want in a separate class in your test project. Your test methods will make several references to these. Let's create a class called WebTestHelper and place these static methods in it.

The first method (actually one method with an override to make two methds) allows you to find a control on a page by referencing it's control ID, the code is as follows:
private Control FindControl(string id)
{
    return FindControl(TestContext.RequestedPage, id);
}
private Control FindControl(Control container, string id)
{
    // See if current control contain's the control we're looking for
    if (container.ID == id) return container;
    Control output = null;
 
    // Loop through child controls and call recursively until found or until end
    if (container.HasControls())
    {
     foreach (Control ctrl in container.Controls)
     {
         output = FindControl(ctrl, id);
         if (output != null) break;
        }
    }
 
    return output;
}
The second method lets you raise events on your controls:
private void RaiseEvent(string eventName, object targetObject, EventArgs args)
{
 var privateObject = new PrivateObject(TestContext.RequestedPage)
 privateObj.Invoke(eventName, targetObject, args);
}
PrivateObject is a class that is part of the Microsoft.VisualStudio.TestTools.UnitTesting namespace. The TestContext object is a private variable that's included in every unit test class generated by Visual Studio.

Dressing up your Test Method(s)

With the above two methods in place you're almost ready to write your first test. You'll need a few attributes above your unit test to get this to work properly.
[TestMethod]
[HostType("ASP.NET")]
[UrlToTest("http://localhost:80/PathToPage.aspx")]
[AspNetDevelopmentServerHost("C:\\Projects\\MyWebProject")]
private void WebPageTest() ...
Fill in the appropriate URL for your .aspx page and the path to your web project.

Writing test code for your .aspx page

We want to test filling in search criteria on the page, clicking search, and then analyze our gridview for data:
// Load controls
var txtCompany = FindControl("txtCompany") as TextBox;
var btnSearch = FindControl("btnSearch") as Button;
var grdCompanies = FindControl("grdCompanies") as GridView; 

// Trigger the search
txtCompany.Text = "Acme";
RaiseEvent("btnSearch_Click", btnSearch, EventArgs.Empty); 

// Run assertions
Assert.IsTrue(grdCompanies.Rows.Count > 0, "No results in search grid");
The result is that our test looks fairly simple and we're referencing our controls explicitly allowing us to run fine-grained tests on them.

No comments:

Post a Comment