
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:
- 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.
- 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.
- logon.aspx:
a web form that allows a user to logon to the website.
- 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:
- All the expected controls are present and visible on the page
- The ValidationSummary
starts out with no error messages
- Clicking Logon without entering data results in all three error messages
in Figure 2.
- Clicking Logon after entering mismatched passwords results in the error
message shown in Figure 3.
- Clicking the “Forgot Your Password” link forwards the user
to forgot.aspx.
- 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:
- Issuse request for http://localhost/nunitasp/securepage.aspx
- Redirected to http://localhost/nunitasp/logon.aspx
- Fill in the form with appropriate credentials
- 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. |
|