Steven Padfield

Spontaneous Tendency for Order
posts - 12, comments - 19, trackbacks - 2

Server-Side Unit Testing in ASP.NET

(this article has been moved from a different blog location)

Server-Side Unit Testing in ASP.NET

How to create an HttpContext outside of IIS

by Steven Padfield

04/13/2004

For those of you who appreciate the robustness and flexibility that .NET has brought to web development, you have undoubtedly hit the same wall I’ve been banging my head against for months. The problem is unit testing, and there seems to be no good way of implementing it in ASP.NET. This article exposes a simple trick (only 6 lines of code), that will let you initialize a valid HttpContext from within your testing harness, allowing you to test code that makes use of ASP.NET intrinsics without having to run the test from within a web form.

In this article I will be using NUnit as my testing framework, but in principle, this technique can apply to any testing framework.

If you want to skip the drama and get straight to the chase, the solution is at the bottom of the article.

Hasn’t this already been done?

There are tools available to do limited unit testing in ASP.NET applications (e.g. NUnitAsp), but they use the tactic of issuing formal HTTP requests and analyzing the responses. They allow you to assert that a particular HTML control appears in the response and that its content matches a certain string of text, and they allow you to verify that clicking a button results in another certain expected response page, etc. Tools like NUnitAsp are a powerful resource for implementing round-trip client-to-server black-box testing.

However, this class of testing is too far removed from the server side to provide any kind of white-box testing for most (if not all) of the classes in any real project. Some classes may not play a direct role in generating the response, or perhaps there are some classes that generate responses too convoluted to analyze with NUnitAsp. What if your application uses the Application Controller pattern, bypassing the entire .NET page hierarchy? Tools such as NUnitAsp will not help you test your infrastructure classes, only the rendered output of individual pages.

If your application is more than a simple photo album, you will probably discover that using a tool like NUnitAsp is not sufficient to cover an acceptable portion of your code base. What we want is a way to test the classes of our application natively on the server side.

You may be thinking, “Why don’t we just write tests in the app and run NUnit against the compiled DLL?” Not so fast! ASP.NET intrinsics are only available when a web request is actively executing, so classes being tested by NUnit will always receive null when they try to access intrinsics. You will find most of your tests fail simply because they are running outside of an IIS context.

Here is a detailed walk-through of the problem and its solution.

The Problem

Say, for example, I have the following class:

public class FileCache
{
public static string GetPrivatePath(string fileName)
{
return HttpContext.Current.Server.MapPath("/webapp/Cache/Private/"
+ filename);
}
}

How do I write a unit test for it? For starters, it would probably look something like this:

[TestFixture]
public class FileCacheTest
{
[Test]
public void GetPrivatePathTest()
{
Assert.AreEqual("c:\\inetpub\\wwwroot\\webapp\\Cache\\Private\\foo.bar",
FileCache.GetPrivatePath("foo.bar"));
}
}

However, upon running the test we see the following exception:

NullReferenceException : Object reference not set to an instance of an object. 

This is not because our logic is wrong. It is because NUnit does not have a valid HttpContext, and so HttpContext.Current returns null. Obviously we need a way to fill this with a valid context.

First Attempt

Our first attempt is a simple one. Just set HttpContext.Current to a new HttpContext object:

[TestFixture]
public class FileCacheTest
{
[Test]
public void GetPrivatePathTest()
{
HttpContext.Current = new HttpContext();
Assert.AreEqual("c:\\inetpub\\wwwroot\\webapp\\Cache\\Private\\foo.bar",
FileCache.GetPrivatePath("foo.bar"));
}
}

Unfortunately, there is no public constructor for HttpContext that takes 0 arguments, so this won’t compile. Looking at our options, we see that we can create an HttpContext if we have an HttpWorkerRequest object. HttpWorkerRequest is an abstract class which has several concrete sub-classes. The one we’ll be using (for obvious reasons) is called SimpleWorkerRequest. With the help of MSDN Library and a little bit of trial-and-error, we arrive here:

[TestFixture]
public class FileCacheTest
{
[Test]
public void GetPrivatePathTest()
{
TextWriter tw = new StringWriter();
HttpWorkerRequest wr = new SimpleWorkerRequest("default.aspx", "", tw);
HttpContext.Current = new HttpContext(wr);

Assert.AreEqual("c:\\inetpub\\wwwroot\\webapp\\Cache\\Private\\foo.bar",
FileCache.GetPrivatePath("foo.bar"));
}
}

Great, our new test compiles. But when we run it, we get a red bar! It’s another NullReferenceException, but this time, it’s coming from the SimpleWorkerRequest constructor.

Now we are stumped: all three parameters being passed to the SimpleWorkerRequest constructor are non-null, so where is this null reference coming from? I had to turn to the use of a very cool and useful tool to help me uncover what was happening behind the scenes.

Behind the Scenes

I used Lutz Roeder’s .NET Reflector (thanks to James Zimmerman at ESS Group, Inc. for showing me this one) to examine what was going on in the .NET Framework that would cause the test to fail. This is a very sleek and very useful tool that lets you reverse compile .NET binaries. I highly recommend adding it to your development arsenal.

In this example, I used .NET Reflector on the .NET Framework itself to see the source code of SimpleWorkerRequest..ctor(string, string, TextWriter). Here is .NET Reflector’s decompilation of that constructor:

public SimpleWorkerRequest(string page, string query, TextWriter output)
{
base..ctor();
InternalSecurityPermissions.UnmanagedCode.Demand();
this._queryString = query;
this._output = output;
this._page = page;
this.ExtractPagePathInfo();
this._appPhysPath = Thread.GetDomain().GetData(".appPath").ToString();
this._appVirtPath = Thread.GetDomain().GetData(".hostingVirtualPath").ToString();
this._installDir = Thread.GetDomain().GetData(".hostingInstallDir").ToString();

this._hasRuntimeInfo = true;
}

We can see that three calls to AppDomain.GetData() are being made. My insight tells me these calls are returning null, and thus the call to ToString() is generating the null reference exception. If we could supply these expected values to the domain before calling the constructor for SimpleWorkerRequest, perhaps these calls would succeed.

That indeed turns out to be the case.

The Solution

[TestFixture]
public class FileCacheTest
{
[Test]
public void GetPrivatePathTest()
{
Thread.GetDomain().SetData(".appPath", "c:\\inetpub\\wwwroot\\webapp\\");
Thread.GetDomain().SetData(".hostingVirtualPath", "/webapp");
Thread.GetDomain().SetData(".hostingInstallDir",
HttpRuntime.AspInstallDirectory);

TextWriter tw = new StringWriter();
HttpWorkerRequest wr = new SimpleWorkerRequest("default.aspx", "", tw);
HttpContext.Current = new HttpContext(wr);
Assert.AreEqual("c:\\inetpub\\wwwroot\\webapp\\Cache\\Private\\foo.bar",
FileCache.GetPrivatePath("foo.bar"));
}
}

Here you can see we’ve added three calls setting data values on the AppDomain. The first one, “.appPath”, we set to the physical path of our web application’s root folder (with trailing back-slash!). The second, “.hostingVirtualPath”, we set to the virtual path of our web application, relative to the server root. The third, “.hostingInstallDir”, we set to the path of the ASP.Net framework, which we get from the static property HttpRuntime.AspInstallDirectory.

Compiling and running the test gives us a green bar.

Caveat

This solution works with version 1.1 of the .NET Framework. As far as I know, this solution uses aspects of the framework which are most likely “undefined” or “unsupported”, meaning they could be changed in such a way as to break my code. I have not verified with Microsoft whether this is the case, yet if it is, it is very likely that no such change would occur until the next version of the .NET Framework. Anyway, just be aware that this is a possibility and keep an eye out whenever you update your framework. 

You are welcome to use any of the code in this article in your own projects. If you do, I would really appreciate if you could drop me a line. Also, please let me know if you encounter any unexpected behaviors when using this technique.

References

posted on Tuesday, April 13, 2004 1:00 PM