Sponsored Links


Resources

.NET Research Library
Get .NET related white papers, case studies and webcasts

Testing ASP.NET Applications with NUnitASP and NUnitTesting ASP.NET Applications with NUnitASP and NUnitTesting ASP.NET Applications with NUnitASP and NUnit Discuss DiscussDiscuss Printer friendly Printer friendlyPrinter friendly
Testing ASP.NET Applications with NUnitASP and NUnit

March 4, 2004

What’s the Problem?

NUnit is an indispensable tool. I use it every day to tell me whether or not I know what I’m doing. Its only major drawback is that the code you test has to be run inside the NUnit test runner process. For most applications, this isn’t a problem. The only “container” you are worried about is the CLR; it doesn’t really matter what the actual executable is. If you need to model external objects for your code’s consumption, you can always knock out some mock objects, either on your own or with a tool like NMock (http://nmock.truemesh.com).

When this does become a problem is trying to test your ASP.NET code. Not the middle-tier logic you tie into, but the .aspx pages themselves. How can you test that the UI reacts to the user correctly? That the postback events happen in the right order? That the correct next page is loaded after the user completes the page? Testing these things requires that your code is running inside the ASP.NET worker process. Your pages need access to the HTTPContext, the Request and Response objects, everything else that ASP.NET provides them at runtime. If you attempt to test your compiled .aspx pages directly from the NUnit test runner, none of them will even load, let alone pass your tests.

NUnitASP is the Solution

Sure, you could write up a bunch of mock objects to convince your pages that they are really running in ASP.NET. However, that list would include:

  • The page “intrinsics”: Request, Response, Application, Server, and Session
  • The webcontrols: TextBox, Button, DropDownList, etc.
  • The context: HTTPContext

You would spend much more time writing this mock framework than writing any unit tests for your own application.

NUNitASP, an open-source (MIT license) application, provides this framework for you. Or, more to the point, it let’s your pages run in the actual ASP.NET worker process, but allows you to create a mock façade container to test the UI with. This façade creates all the server-side web- and html-control objects present on your page, whose properties you can manipulate and whose events you can fire. Then, you can check the results (of a postback or cross-page navigation).

To allow for this testing, NUnitASP has to provide not only a mock container for your pages but a browser imitation object that acts as the requesting client. The browser imitation object is what allows you to test the current URL, the cookies collection, and the static HTML output of a request. The collection of mock server controls allows you to manipulate and test the server-side properties and behavior of your pages.

Getting Started

First, you will need to download the latest version of NUnitASP (http://nunitasp.sourceforge.net/download.html). The download comes with an installer for the latest version of NUnit, which you will also need if you don’t already have it installed. To install, NUNitASP, simply unzip it into the folder of your choice and you are done.

To get started using it, create a new ASP.NET Web Application project (or load up an existing one that you want to test) and add references to nunit.framework.dll (in the NUnit install’s bin folder) and nunitasp.dll (in the NUnitASP install’s bin folder). For this article, we’ll be creating a simple application consisting of four pages:

  1. default.aspx: the main entry point into our application. If the user is logged on, it will display their name. Otherwise, the label containing the user name will be invisible.
  2. securepage.aspx: a web form that is marked as <deny users=”?”/> in the web.config file. This means that unauthenticated users cannot load the page.
  3. logon.aspx: a web form that allows a user to logon to the website.
  4. forgot.aspx: the web form to allow users who have forgotten their password to retrieve it or create a new one.

The web.config file for our application has the following sections in it:

<authentication mode="Forms">
      <forms loginUrl="logon.aspx"/>
</authentication>
<authorization>
     	<allow users="*" />
</authorization>

<location path="securepage.aspx">
<system.web>
		<authorization>
			<deny users="?"/>
		</authorization>
	</system.web>
 </location>

These sections establish that we are using Forms Authentication for our security mechanism, that “logon.aspx” should be loaded whenever a non-authenticated user attempts to access a page.

Testing Our First Page

We’ll start by testing the logon.aspx page, since the rest of our navigation tests will depend on it. Figure 1 shows logon.aspx on first access, before the user has posted any data to it.


Figure 1. The logon page.

All three text fields are required fields, and have associated RequiredFieldValidators. If the user clicks the Logon button without entering anything, they get the page shown in Figure 2.


Figure 2. Logon form showing required-field errors.

If the user enters a username and the password (twice), but the passwords don’t match, they should see the page in Figure 3.


Figure 3. Logon page with mismatched passwords.

To test the page, we need to ensure the following functionality:

  1. All the expected controls are present and visible on the page
  2. The ValidationSummary starts out with no error messages
  3. Clicking Logon without entering data results in all three error messages in Figure 2.
  4. Clicking Logon after entering mismatched passwords results in the error message shown in Figure 3.
  5. Clicking the “Forgot Your Password” link forwards the user to forgot.aspx.
  6. Clicking Logon with correct data entered forwards the user to “default.aspx” (the ASP.NET default destination if the logon page is used directly, not in response to a request for a secured page)

Writing the Test Case

Add a standard NUnit test case class to the application. Ours is called TestLogon. I usually add a using statement for NUnit.Framework, NUnit.Extensions.ASP, NUnit.Extensions.Asp.AspTester and nunitasp. The major difference between an NUnitASP test case and a regular NUnit test case is that the NUnitASP test case has to derive from NUnit.Extensions.Asp.WebFormTestCase in addition to being decorated with the [TestFixture] attribute.

using System;
using NUnit.Framework;
using NUnit.Extensions.Asp;
using NUnit.Extensions.Asp.AspTester;
using nunitasp;

[TestFixture]
public class TestLogon : NUnit.Extensions.Asp.WebFormTestCase
{

}

Another major difference is that your NUnitASP test case cannot use the [SetUp] and [TearDown] attributes to create the test lifecycle methods. The base class WebFormTestCase is already using them to establish the context and mock objects for your page; re-implementing them would cause them to hide the base class definitions, causing your tests to fail. Instead, you will create override methods for SetUp and TearDown, like so:

Protected override void SetUp()
{
}

Protected override void TearDown()
{
}

We’ll look at the contents of those functions in a minute. First, let’s write our first actual test, to see if the page loads and the appropriate controls are visible. The test method has to be decorated with the [Test] attribute, in order for the test runner to see it.

[Test]
public void TestLoad()
{
}

To perform the test, we first have to load the page, then create the mock server side controls, then finally examine their attributes. The base class for our test case, WebFormTestCase, exposes a protected Browser property, which returns an instance of HttpClient, our browser imitator object. To load the page, call the Browser.GetPage() method, passing in the local (or remote) URL to the desired page.

Browser.GetPage(“http://localhost/nunitasp/logon.aspx”);

Loading the Server-side Controls

Once we have invoked the page, we can create our mock server-side controls to test their properties and methods. Just as ASP.NET categorizes controls into two different namespaces, WebControls and HtmlControls, NUnitASP provides two different namespaces for the mock objects:

  • NUnit.Extensions.Asp.Asptester: mock objects for the WebControls namespace
  • NUnit.Extensions.Asp.HtmlTester: mock objects for the HtmlControls namespace

To create one, you need to instantiate the appropriate mock control class, passing in the id of the control on the page and a scoping container. The scoping container is the control that is the direct parent of the target control. For most controls, the scoping container is the page itself. For this case, the WebFormTestCase class exposes another property, CurrentWebForm (of type NUnit.Extensions.Asp.WebForm) which you can pass into the method as the second parameter. When the control’s parent is another control on the page, you must first create the parent control’s mock server object and pass that as the second parameter to the constructor.

For our page, all of the controls are direct children of the page, so to create them, the code looks like this:

TextBoxTester txtname = new TextBoxTester("txtName", CurrentWebForm);
TextBoxTester txtpwd1 = new TextBoxTester("txtPwd1", CurrentWebForm);
TextBoxTester txtpwd2 = new TextBoxTester("txtPwd2", CurrentWebForm);
ButtonTester btnlogon = new ButtonTester("btnLogon", CurrentWebForm);
ValidationSummaryTester valsum = new ValidationSummaryTester("valsum",
CurrentWebForm);
AnchorTester hlforgot = new AnchorTester("hlForgot", CurrentWebForm, true);

The only line that differs in that list is the last one. AnchorTester is used as the mock control for both the WebControls.HyperLink class and the HtmlControls.HtmlAnchor class. The third parameter to the constructor is a boolean specifying whether the control runs on the server or not. Our is a server-side HyperLink control, so we pass true.

Once we have all the mock objects created, we can test the appropriate visibility settings. Since we have just loaded the page for the first time, everything should be visible except the ValidationSummary.

AssertVisibility(txtname, true);
AssertVisibility(txtpwd1, true);
AssertVisibility(txtpwd2, true);
AssertVisibility(btnlogon, true);
AssertVisibility(hlforgot, true);
AssertVisibility(valsum, false);

That’s it. Our test is completed. We can now ensure that the page loads to the appropriate initial state whenever a user makes their first request for it.

Appropriate Factoring

Right away, you can see that there will be a lot of repetitive code if we add more tests to our test case. Each and every test method has to setup and access the mock server objects for our page. Instead of doing them in every method, we should employ the SetUp method to do the work. Remember that the SetUp method is called by the test runner before each individual test method.

Our class now looks like:

TestFixture]
public class TestLogon : NUnit.Extensions.Asp.WebFormTestCase
{
	TextBoxTester txtname;
	TextBoxTester txtpwd1;
	TextBoxTester txtpwd2;
	ButtonTester btnlogon;
	ValidationSummaryTester valsum;
	AnchorTester hlforgot;

	protected override void SetUp()
	{
	 	txtname = new TextBoxTester("txtName", CurrentWebForm);
		txtpwd1 = new TextBoxTester("txtPwd1", CurrentWebForm);
		txtpwd2 = new TextBoxTester("txtPwd2", CurrentWebForm);
		btnlogon = new ButtonTester("btnLogon", CurrentWebForm);
		valsum = new ValidationSummaryTester("valsum", CurrentWebForm);
		hlforgot = new AnchorTester("hlForgot", CurrentWebForm, true);
	}

	// Rest of class
	// ...
}

In addition, we’ll probably want to provide a series of string constants for comparing URLs to in our other tests.

const string mypage = "http://localhost/nunitasp/logon.aspx";
const string defaultpage = "http://localhost/nunitasp/default.aspx";
const string forgotpage = "http://localhost/nunitasp/forgot.aspx";

Now, our test is a nice, compact method that focuses on the expected behavior, not all the loading of controls.

[Test]
public void TestLoad()
{
	Browser.GetPage(mypage);

	AssertVisibility(txtname, true);
	AssertVisibility(txtpwd1, true);
	AssertVisibility(txtpwd2, true);
	AssertVisibility(btnlogon, true);
	AssertVisibility(hlforgot, true);
	AssertVisibility(valsum, false);
}

Testing Behavior

Now that we’ve ensured appropriate initial state, we need to see how the page reacts to user input. First, we need to verify that the validator controls work properly if the user clicks the logon button without having entered any data. Any controls that expose server-side events have appropriate methods on the mock objects for invoking them.

[Test]
public void TestNoName()
{
	Browser.GetPage(mypage);

	btnlogon.Click();

	AssertVisibility(valsum, true);
	AssertEquals(valsum.Messages[0].ToString(), "Password required.");
	AssertEquals(valsum.Messages[1].ToString(), "Username required.");
	AssertEquals(valsum.Messages[2].ToString(), "Password 2 required.");
}

After firing the click event of the button, we have created a postback to the server and caused our CurrentWebForm to contain the new response. We can then test the properties of our server controls to see if they reacted appropriately. First, our ValidationSummary should now be visible. Secondly, it should have three error messages in its Messages collection. The order of the messages is the order the individual validation controls that spawn those messages were added to the page. We just use AssertEquals to ensure that each message is what we expect it to be. Similarly, we could always just check to make sure the Messages collection has the appropriate number of entries by querying the Messages.Length property. This would be less specific, but faster, and we wouldn’t have to worry about the ordering of the error messages.

Similarly, we also need to test other combinations of input. To test for the scenario shown in Figure 3 above, we add a username value and two mismatched strings for the passwords.

[Test]
public void TestBadPwd()
{
	Browser.GetPage(mypage);

	txtname.Text = "Justin";
	txtpwd1.Text = "Justin";
	txtpwd2.Text = "asdfasd";
	btnlogon.Click();

	AssertVisibility(valsum, true);
	AssertEquals("Passwords not equal.", valsum.Messages[0].ToString());
}

In this instance, we add values to the Text properties of the three TextBoxTester controls, then fire the btnlogon.Click() event. Again, the ValidationSummary should be visible, but now it should only contain a single message, as shown.

Finally, we need to verify that the page does the right thing when we enter all the right values (currently, the “authentication” mechanism checks to see if the password is the same as the username). When the user enters all the appropriate information and clicks the logon button, they should be forwarded to default.aspx, which is the ASP.NET default when the logon page is used directly.

For this test, when the logon button is clicked, the client is redirected to default.aspx. In this case, “the client” refers to our instance of HttpClient, Browser. Browser exposes a public property called CurrentUrl, which we will verify.

[Test]
public void TestGoodVals()
{
	Browser.GetPage(mypage);

	txtname.Text = "Justin";
	txtpwd1.Text = "Justin";
	txtpwd2.Text = "Justin";

	btnlogon.Click();

	AssertEquals(Browser.CurrentUrl, defaultpage);
}

Last, we also need to verify that the “Forgot your password?” link works as planned:

[Test]
public void TestForgot()
{
	Browser.GetPage(mypage);

	NUnit.Extensions.Asp.HtmlTester.AnchorTester anchor =
		new NUnit.Extensions.Asp.HtmlTester.AnchorTester("hlForgot", CurrentWebForm, true);
	AssertNotNull(anchor);
	AssertEquals(anchor.HRef, "forgot.aspx");
	AssertVisibility(anchor, true);
	Assertion.Assert(!anchor.Disabled);
	anchor.Click();
	AssertEquals(Browser.CurrentUrl, forgotpage);
}

That’s it. A real logon page would have made use of some more sophisticated authentication mechanism, possibly looking up values in LDAP or checking a hash of the password against a custom database, etc. Such a library of code should also be thoroughly unit tested, but those tests will be standard NUnit tests since the business logic should have nothing to do with the web UI.

A rule of thumb: only test those controls whose values are likely to, or even able to, change over the course of a user interaction with the page. Generally, static controls (even server side controls with static content) are unlikely to cause problems with the page and checking every single one will make your unit tests a chore instead of a pleasure.

Testing Navigation and Page Flow

Now that we’ve handled the basics of testing a page, we can apply the same techniques to any other page in our application. However, testing page state and simply page forwarding does not tell us the whole story about a website. Take the example of a user who attempts to navigate to securepage.aspx. If you remember from our web.config file above, securepage.aspx is configured to only allow authenticated users to access it.

The standard page flow for a user issuing a non-authenticated request for securepage.aspx is:

  1. Issuse request for http://localhost/nunitasp/securepage.aspx
  2. Redirected to http://localhost/nunitasp/logon.aspx
  3. Fill in the form with appropriate credentials
  4. Redirected back to http://localhost/nuniasp/securepage.aspx

A proper test will attempt to complete the entire process. (A really good suite of unit tests will test each page individually, like in the first part of this article, and then layer combination tests like this one on top of them.)

[TestFixture]
public class TestSecurePage : NUnit.Extensions.Asp.WebFormTestCase
{
const string mypage = "http://localhost/nunitasp/securepage.aspx";
	const string loginpage = "http://localhost/nunitasp/logon.aspx";

	[Test]
public void TestLoad()
	{
	Browser.GetPage(mypage);

Assertion.Assert(Browser.CurrentUrl.ToString().StartsWith(loginpage));

	TextBoxTester txtname = new TextBoxTester("txtName", CurrentWebForm);
	TextBoxTester txtpwd1 = new TextBoxTester("txtPwd1", CurrentWebForm);
	TextBoxTester txtpwd2 = new TextBoxTester("txtPwd2", CurrentWebForm);
	ButtonTester btnlogon = new ButtonTester("btnLogon", CurrentWebForm);
	txtname.Text = "Justin";
	txtpwd1.Text = "Justin";
	txtpwd2.Text = "Justin";
	btnlogon.Click();

	AssertEquals(mypage, Browser.CurrentUrl.ToString());
	}
}

When we initially request securepage.aspx, we should be redirected to logon.aspx. Our first assertion verifies that this is the case. Next, we have to fill in the logon form with the appropriate values (by first created mock objects for the three TextBoxes and the Button). Finally, we click the logon button and verify that we ended up back at securepage.aspx.

Note the first assertion in the test:

Assertion.Assert(Browser.CurrentUrl.ToString().StartsWith(loginpage));

Instead of just checking the entire URL, we use the StartsWith method. This is because the redirected URL that the user sees when they reach logon.aspx through this mechanism contains the originally requested url as a query string parameter. Instead of checking that whole string, we generically just check that we have been forwarded to the right page but just examining the base of the URL.

Refactoring the Page Flow Tests

As your site adds more and more pages that require authentication and for whom you want to ensure that the redirection works properly, you’ll code more and more tests that have to fill out the logon form with correct values. This kind of thing can happen in other ways as well, when multiple navigation pathways contain the same form to fill in or user interactions to trap. To make our tests as lean as possible, and avoid writing redundant code, it is a good practice to make a class that handles these standard interactions in a central location. I usually call this class AutoFill, and supply methods for each web form I want to handle.

Here is the AutoFill class with a method for handling our logon page.

public class AutoFill
{
	public void FillLogonCorrectly(WebForm CurrentWebForm)
	{
	TextBoxTester txtname = new TextBoxTester("txtName", CurrentWebForm);
	TextBoxTester txtpwd1 = new TextBoxTester("txtPwd1", CurrentWebForm);
	TextBoxTester txtpwd2 = new TextBoxTester("txtPwd2", CurrentWebForm);
	txtname.Text = "Justin";
	txtpwd1.Text = "Justin";
	txtpwd2.Text = "Justin";
	}
}

Each method on the AutoFill class takes a single parameter, an instance of WebForm. Using this instance, we create the controls we’ll need and manipulate their properties accordingly. There is no need for a return value, since whoever passed in the reference to WebForm will have their own reference to it, which will now contain the new values.

Using this class, our test method for securepage.aspx now looks like this:

[Test]
public void TestLoad()
{
	Browser.GetPage(mypage);
		Assertion.Assert(Browser.CurrentUrl.ToString().StartsWith(loginpage));

	AutoFill autofill = new AutoFill();
	autofill.FillLogonCorrectly(CurrentWebForm);

	ButtonTester btnlogon = new ButtonTester("btnLogon", CurrentWebForm);
	btnlogon.Click();

	AssertEquals(mypage, Browser.CurrentUrl.ToString());
}

Not only is this test cleaner and more concise, but any other test that needs to fill in the logon form can call the same method. If the logon form ever changes (the number, name or type of controls are changed) we can merely modify our central AutoFill.FillLogonCorrectly() method to handle the new structure. Generally, for every FillXXXCorrectly method, you’ll also want a FillXXXIncorrectly method to quickly autofill with inappropriate values, as well as a FillXXX(paramlist) method to allow other tests to pass in the values to use for the form.

Where to Next

For more information on the full NUnitASP programming model, you can check out their API documents at http://nunitasp.sourceforge.net/api.html. The model contains other mock server controls, including ones for grids and tables which allow you to access their contents as arrays of objects. (Not all the WebControls have had mock controls implemented for them yet. The product is still in development.)

In my last three columns, we’ve covered unit testing with NUnit, build management with NAnt, and web UI testing with NUnitASP. Next month, we’re going to look at various continuous integration solutions that allow us to combine these three techniques with assured as-soon-as-possible build and integration testing whenever we make changes to our code.

Authors

Justin Gehtland is a founding member of Relevance, LLC, a consultant group dedicated to elevating the practice of software development. He is the co-author of Windows Forms Programming in Visual Basic .NET (Addison Wesley, 2003) and Effective Visual Basic (Addison Wesley, 2001). Justin is an industry speaker, and instructor with DevelopMentor in the .NET curriculum.

News | Blogs | Discussions | Tech talks | White Papers | Downloads | Articles | Media kit | About
All Content Copyright ©2007 TheServerSide Privacy Policy
Site Map