Published: 12 Jan 2011
By: Kenneth Uildriks

This article outlines one approach to mocking ASP.NET and freeing your unit tests and your continuous integration server from dependencies on IIS and virtual directory configuration.

Contents [hide]

Introduction

Test-driven development leads to more reliable code, and anything that can be unit tested should be unit tested. Unfortunately, some frameworks, such as ASP.NET, do not lend themselves to being mocked in order to support unit tests. This is a particular issue with HttpModules and HttpHandlers, which by their nature must integrate heavily with difficult-to-mock ASP.NET objects that typically exist only within IIS. This article outlines one approach to mocking ASP.NET and freeing your unit tests and your continuous integration server from dependencies on IIS and virtual directory configuration.

The HttpModule

An HttpModule is usually designed to execute whenever a Web request begins, and complete its work before any HttpHandlers or ASPX pages begin executing. It will nearly always need to look at one or more request headers and take action based on their values; thus, it cannot run without this information. In order to unit test it, it is best if the modules that it depends on (chiefly the ASP.NET framework) can be mocked, so that test values can be passed to the HttpModule within the process of a unit test framework such as NUnit and tests can be written to check its behavior with a wide variety of client requests. However, it is nearly impossible to create valid well-behaved instances of HttpContext, HttpApplication, HttpRequest, or HttpResponse outside of an ASP.NET application host. Fortunately, it is possible to create an ASP.NET application host, which can in turn create these ASP.NET objects for your tests.

The ASP.NET Application Host

An Application Host begins the entire ASP.NET request processing machinery. The Application Host lives in an AppDomain specially created for it, and it creates an HttpWorkerRequest to encapsulate all of the details of getting request data from the client and sending response data back. The Application Host then feeds the HttpWorkerRequest instance to HttpRuntime.ProcessRequest, which creates an HttpContext and invokes your HttpModules and HttpHandlers.

Creating and using an Application Host

An Application Host must live in a specially configured AppDomain, and a single method is provided that creates the AppDomain, configures it for ASP.NET, and creates the Application Host within that domain. These operations cannot be done individually.

The physical directory parameter must point to an ASP.NET application directory, the one that would be mapped to the corresponding virtual directory in IIS. Within the newly-created AppDomain, assemblies will be looked up in the "bin" folder directly under the given physical directory. ASP.NET will look for .aspx pages and other content relative to the given physical directory.

You are free to choose the virtual directory name; the HttpRequest that gets created will include that virtual directory within the request URL that it reports.

The first parameter is a class that derives from MarshalByRefObject. An instance of that class is created within the newly-created AppDomain, and a remote reference to that instance is returned from the call to CreateApplicationHost.

Unit Testing with an Application Host

Since the Application Host must live in its own AppDomain which will look for assemblies within the bin folder of the ASP.NET application's physical directory, the easiest way to get unit test code to run within that AppDomain is to put it inside of an ASP.NET project. So we'll create an ASP.NET project called WebObjectTesting, and give that project a reference to nunit-framework.dll.

Now add a Test class to the project and decorate it with the [TestFixture] attribute. Add an empty test method and compile the project.

Load WebObjectTesting.dll into NUnit and make sure that NUnit recognizes and successfully runs the test.

Testing the Application Host

Before we can test anything that runs inside an application host, we need to successfully get an application host running. The first step is to define the class that will be instantiated within the Application Host AppDomain:

Since it lives in a separate AppDomain, it must extend MarshalByRefObject, and it will be called using .NET Remoting. This means that every method you define on that class should have all of its parameter types and its return type be serializable.

It also means that NUnit cannot directly interact with this instance; any unit test methods you define will have to run in the original AppDomain created by NUnit and interact with the TestDriver class through .NET Remoting.

We'll start by giving TestDriver a simple Go method:

This simply sets up the worker request and calls ProcessRequest with the given page and query string. To test it, we need to add a test method to the Test class (not the TestDriver class) that calls it remotely:

Compile, load with NUnit, and run the test.

Figure 1: No failures and a red bar?

No failures and a red bar?

NUnit reports the test passing, and still gives us a red bar!

Now what?

After the request is finished, NUnit unloads the AppDomain, but ASP.NET is not quite finished with it. To tell ASP.NET that all request processing is done, you must call HttpRuntime.Close() within the Application Host's App Domain. So let's give TestDriver another method to do just that:

And call it remotely after we're done with our test:

Recompile and try it with NUnit again:

Figure 2: Success

Success

Now we get our green bar.

Was the application processing successful?

However, just because we got a green bar doesn't mean that the whole thing went off without a hitch. Recall that when an exception occurs within ASP.NET, the exception doesn't bubble all the way up and crash the Web server; instead, ASP.NET catches the exception and renders an error page with an HTTP error response code. So in order to make sure that everything within ASP.NET ran successfully, we need to check the error code.

This is where Rhino Mocks comes in. We can create a partial mock of the SimpleWorkerRequest and set expectations and explicit returns on certain method calls while allowing other method calls to pass through to SimpleWorkerRequest. In this case, we know that ASP.NET will call SendStatus on the worker request object, and will pass it the HTTP status code. After adding a reference to Rhino Mocks, we simply set an expectation that that status code is equal to 200 (HTTP for "OK"), and the mock object will throw an exception if that expectation isn't met:

Now compile it and run NUnit:

Figure 3: Expected HTTP OK, got HTTP error!

Expected HTTP OK, got 

HTTP error!

The test is red! What went wrong?

One way to find out is to Debug->Attach to the NUnit process (probably nunit-agent.exe, unless you've configured NUnit differently) and set a breakpoint right after ProcessRequest and look at the value of "sw".

Figure 4: Getting the page rendered by ASP.NET

Getting the page 

rendered by ASP.NET

It should give us the HTML text of the error page, which in this case tells us that Test.aspx doesn't exist.

Figure 5: sw.ToString() run through Visual Studio's HTML visualizer

sw.ToString() run through Visual Studio's HTML visualizer

This gives us confidence that we're actually testing what we think we're testing, and reminds us to create Test.aspx. Create Test.aspx, recompile, and run the test again. This time it should pass.

Adding an HttpModule

Now it's time to test an actual HttpModule. It needs to live in its own project so we can reference it from our unit test project and from the real ASP.NET project that needs to use it. Create the MyHttpModule project as a Class Library.

In our example, the HttpModule will simply keep track of how many requests the application has gotten from this particular client with the help of a cookie. If the client already has a RequestNumber cookie, it will add one to the number and reset the cookie; otherwise it'll create a brand-new RequestNumber cookie with the value 1:

There's a bug in it, but we'll find and fix it later with the help of a unit test.

Now in the ASP.NET project, we need to add a reference to the MyHttpModule project. Also, in order to actually use the HttpModule, we need to change the Web.config:

NOTE

In IIS 7, the default is to run ASP.NET applications in the Integrated Pipeline. However, custom Application Hosts cannot use the Integrated Pipeline; they can only run ASP.NET applications with the Classic Pipeline. The Web.config syntax for the Classic Pipeline is different from that needed for the Integrated Pipeline. The example sets the same HttpModule for both pipelines.

Compile and run the unit test again. It should still be green.

Testing your HttpModule

At this point, the unit test tells us that ASP.NET didn't encounter any errors; it doesn't tell us that the HttpModule did the right thing, or that it even executed. To check that, we need to add more expectations:

Here we make sure that the HttpModule executed and properly set the cookie in response to a request that didn't have the cookie. Compile it and run NUnit.

Figure 6: Not quite working yet

Not quite working yet

Now it's red. The expectation wasn't met. What happened? What cookie value did we get?

A quick way to find out is to force RhinoMock to call a custom delegate whenever SendKnownResponseHeader is called with any other cookie value:

Now set a breakpoint, attach the debugger, and try the test again.

Figure 7: That's not supposed to be the cookie value!

That's not 

supposed to be the cookie value!

The request number was set to two! How did that happen? Fortunately, we can set a breakpoint in the HttpModule itself and try again:

Figure 8: Found it!

Found it!

We now see that in the case where the client doesn't have the cookie set, the HttpModule sets requestNumber to one, and then adds one to it! It needs to set requestNumber to zero instead. Fix the bug, recompile and run the test.

Figure 9: Bug is fixed

Bug is fixed

And back to green.

Now let's reorganize the test project so that we can quickly code multiple tests:

And extend our Go method so that it can optionally submit a request number cookie and check that we get the right cookie back:

Add NUnit test methods to call it twice, once with no cookie, and once with a RequestNumber cookie value. Compile and run the tests again.

Figure 10: Both tests passing

Both tests passing

Summary

Following this pattern, you can unit test not only HttpModules, but any code that must run in ASP.NET. You can even unit-test existing code that's in code-behind – and, with unit tests in place, refactor it without breaking it.

<<  Previous Article Continue reading and see our next or previous articles Next Article >>

About Kenneth Uildriks

Sorry, no bio is available

This author has published 2 articles on DotNetSlackers. View other articles or the complete profile here.

Other articles in this category


Code First Approach using Entity Framework 4.1, Inversion of Control, Unity Framework, Repository and Unit of Work Patterns, and MVC3 Razor View
A detailed introduction about the code first approach using Entity Framework 4.1, Inversion of Contr...
jQuery Mobile ListView
In this article, we're going to look at what JQuery Mobile uses to represent lists, and how capable ...
Exception Handling and .Net (A practical approach)
Error Handling has always been crucial for an application in a number of ways. It may affect the exe...
JQuery Mobile Widgets Overview
An overview of widgets in jQuery Mobile.
Book Review: SignalR: Real-time Application Development
A book review of SignalR by Simone.

You might also be interested in the following related blog posts


Testability vs. Testing read more
Agile Testing tools List read more
The evolution of unit testing and syntax read more
NDemo - Demo Driven Development? read more
Structuring Unit Test Code read more
The future of unit testing tools - MbUnit, NUnit, NMock and FIT read more
TDD Lesson 4 - I mock you (updated) read more
TDD Lessson 4 - I mock you! read more
Becoming more Agile : I'm learning TDD read more
Unit testing demonstrated level 300 read more
Top
 
 
 

Discussion


Subject Author Date
placeholder question Net 205 10/30/2011 2:41 PM

Please login to rate or to leave a comment.