Simulating Http Context For Unit Tests Without Using Cassini nor IIS

UPDATE: I have recently posted a newer and better version of this code on my blog.

As I’ve stated before, I’m a big fan of completely self-contained unit tests. When unit testing ASP.NET pages, base classes, controls and such, I use the technique outlined by Scott Hanselman in this post. In fact, several of the unit tests for RSS Bandit use this technique in order to test RSS auto-discovery and similar features.

However, there are cases when I want a “lightweight” quick and dirty way to test library code that will be used by an ASP.NET project. For example, take a look at this very contrived example that follows a common pattern...

/// <summary>

/// Obtains some very important information.

/// </summary>

/// <returns></returns>

public SomeInfo GetSomeInfo()

{

    SomeInfo info = HttpContext.Current.Items["CacheKey"] as SomeInfo;

    if(info == null)

    {

        info = new SomeInfo();

        HttpContext.Current.Items["CacheKey"] = info;

    }

    return info;

}

The main purpose of this method is to get some information in the form of the SomeInfo class. Normally, this would be very straightforward to unit test except for one little problem. This method has a side-effect. Apparently, it’ll cost you to obtain this information, so the method checks the Context’s Items dictionary (which serves as the current request’s cache) first before paying the cost to create the SomeInfo instance. Afterwards it places that instance in the Items dictionary.

If I try and test this method in NUnit, I’ll run into a NullReferenceException when attempting to access the static Current property of HttpContext.

One option around this is to factor the logic between the caching into its own method and test that. But in this case, I want to test that the caching works and doesn’t cause any unintended consequences.

Another option is to fire up Cassini in my unit test, create a website that uses this code, and test the method that way, but that’s a “heavy” (and potentially very indirect) way to test this method.

As I stated before, I wanted a “lightweight” means to test this method. There wouldn’t be a problem if HttpContext.Current was a valid instance of HttpContext. Luckily, the static Current property of HttpContext is both readable and writeable. All it takes is to set that property to a properly created instance of HttpContext. However, creating that instance wasn’t as straightforward as my first attempt. I’ll spare you the boring details and just show you what I ended up with.

I wrote the following static method in my UnitTestHelper class. All the write statements to the console shows the values for commonly accessed properties of the HttpContext Note that this method could be made more general for your use. This is the version within Subtext.

/// <summary>

/// Sets the HTTP context with a valid simulated request

/// </summary>

/// <param name="host">Host.</param>

/// <param name="application">Application.</param>

public static void SetHttpContextWithSimulatedRequest(string host, string application)

{

    string appVirtualDir = "/";

    string appPhysicalDir = @"c:\projects\SubtextSystem\Subtext.Web\";

    string page = application.Replace("/", string.Empty) + "/default.aspx";

    string query = string.Empty;

    TextWriter output = null;

 

    SimulatedHttpRequest workerRequest = new SimulatedHttpRequest(appVirtualDir, appPhysicalDir, page, query, output, host);

    HttpContext.Current = new HttpContext(workerRequest);

 

    Console.WriteLine("Request.FilePath: " + HttpContext.Current.Request.FilePath);

    Console.WriteLine("Request.Path: " + HttpContext.Current.Request.Path);

    Console.WriteLine("Request.RawUrl: " + HttpContext.Current.Request.RawUrl);

    Console.WriteLine("Request.Url: " + HttpContext.Current.Request.Url);

    Console.WriteLine("Request.ApplicationPath: " + HttpContext.Current.Request.ApplicationPath);

    Console.WriteLine("Request.PhysicalPath: " + HttpContext.Current.Request.PhysicalPath);

}

You’ll notice this code makes use of a class named SimulatedHttpRequest. This is a class that inherits from SimpleWorkRequest which itself inherits from HttpWorkerRequest. Using Reflector, I spent a bit of time looking at how the HttpContext class implements certain properties. This allows me to tweak the SimulatedHttpRequest to mock up the type of request I want. The code for this class is...

/// <summary>

/// Used to simulate an HttpRequest.

/// </summary>

public class SimulatedHttpRequest : SimpleWorkerRequest

{

    string _host;

 

    /// <summary>

    /// Creates a new <see cref="SimulatedHttpRequest"/> instance.

    /// </summary>

    /// <param name="appVirtualDir">App virtual dir.</param>

    /// <param name="appPhysicalDir">App physical dir.</param>

    /// <param name="page">Page.</param>

    /// <param name="query">Query.</param>

    /// <param name="output">Output.</param>

    /// <param name="host">Host.</param>

    public SimulatedHttpRequest(string appVirtualDir, string appPhysicalDir, string page, string query, TextWriter output, string host) : base(appVirtualDir, appPhysicalDir, page, query, output)

    {

        if(host == null || host.Length == 0)

            throw new ArgumentNullException("host", "Host cannot be null nor empty.");

        _host = host;

    }

 

    /// <summary>

    /// Gets the name of the server.

    /// </summary>

    /// <returns></returns>

    public override string GetServerName()

    {

        return _host;

    }

 

    /// <summary>

    /// Maps the path to a filesystem path.

    /// </summary>

    /// <param name="virtualPath">Virtual path.</param>

    /// <returns></returns>

    public override string MapPath(string virtualPath)

    {

        return Path.Combine(this.GetAppPath(), virtualPath);

    }

}

Within the SetUp method of my TestFixture, I call this method like so...

[SetUp]

public void SetUp()

{

    _hostName = UnitTestHelper.GenerateUniqueHost();

    UnitTestHelper.SetHttpContextWithBlogRequest(_hostName, "MyBlog");

}

Unfortunately, this so called “lightweight” approach has its limits. Any call in your code to HttpContext.Currert.Request.MapPath will throw an exception. I tried working around this, but it looks like I’m at an impasse. The MapPath method makes use of the HttpRuntime.AppDomainAppPath property. Unfortunately, I cannot simulate the HttpRuntime in a lightweight manner. There is a way to run the code being tested within an HttpRuntime, but that, of course, is the heavyweight Cassini method mentioned above.

[Listening to: The Joint/Wavefrint - DJ Hyper - Bedrock Breaks CD1 (5:35)]

What others have said

Requesting Gravatar... Yan Jun 15, 2005 10:47 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Thanks for the information.
Although ASP Unit is more suited for testing web forms applications, I never somehow got to using it. So your idea is great.
Requesting Gravatar... Radomirs Jun 22, 2005 7:46 AM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Help!

How to simulate context for POST request with some posted data?!
Requesting Gravatar... TN Jul 05, 2005 3:02 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Hey,

Do you have this implemented in VB.NET?
Requesting Gravatar... haacked Jul 05, 2005 4:08 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Nope, but it shouldn't be too hard to convert this to VB.NET.
Requesting Gravatar... andyBlog Aug 04, 2005 8:14 AM
# Simulating Http Context For Unit Tests without using a server.
Interesting approach from Phil. Something I'll certainly be looking to take advantage of.
Requesting Gravatar... Kara May 03, 2006 12:02 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
I don't see the SimpleWorkRequest which itself inherits from HttpWorkerRequest... The HttpWorkerRequest class does not take in parameters when creating the object...

No overload for method 'HttpWorkerRequest' takes '5' arguments
Requesting Gravatar... Haacked May 04, 2006 10:02 AM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
The SimpleWorkerRequest class is in the namespace System.Web.Hosting. Make sure you reference the System.Web assembly.

Requesting Gravatar... Iron Yuppie May 23, 2006 4:19 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
couple of quick questions:

1) How do you access a specific page (vs. the default page)
2) UnitTestHelper.GenerateUniqueHost() what does the output of this looklike?
Requesting Gravatar... Ronny Ortega Jun 07, 2006 10:37 AM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Hi everybody!

I tried this solution, but my code access the method 'HttpContext.Current.Profile.GetProfileGroup' and the readonly property 'Profile' is null.

So, any ideas how can I simulate the 'Profile' object. The problem is that this property is read-only.

Regards!
Ronny

Requesting Gravatar... Haacked Jun 07, 2006 12:47 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Unfortunately I haven't tried to simulate that yet.
Requesting Gravatar... Florian Krüsch Aug 23, 2006 3:19 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
Funny, encouraged by one of your last blog posts, I've just started what is going to be my first MVP style ASP.net Webapp and ran into exactly into the same problem .
I'm storing some temporary data in the session store and was wondering how I could deal with a dependecy on HttpContext when unittesting this piece of code.
Thanks for sharing, this looks like a good approach.
Requesting Gravatar... haacked Aug 23, 2006 3:35 PM
# re: Simulating Http Context For Unit Tests Without Using Cassinni nor IIS
In the comments of that post on MVP, a commenter mentions implementing your own IContext and then links to a full implementation.

In principle, that would be the way to go, depending on your needs. If Session is the only thing you need, then I would consider only abstracting out the session by creating your own session class that can either store in ASP.NET session or a test class of your choosing.

Or download my code to simulate the Http Context and use that. I think it handles session. If not, you could probably add it.
Requesting Gravatar... Community Blogs Dec 12, 2006 9:24 AM
# Using WebServer.WebDev For Unit Tests
Last night a unit test saved my life ( with apologies ) . Ok, maybe not my life, but the act&#160;of writing
Requesting Gravatar... Sergey Rybalkin Feb 07, 2007 3:59 PM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
First of all thank you for the great article!

This is what I was looking for some time ago but unfortunately didn't find:-) So I had to implement my own SimpleWorkerRequest derived class. Regarding the issue with simulating the HttpRuntime - you can set all the required properties through reflection. I did it like this -
// get singleton property value
FieldInfo field = typeof(HttpRuntime).GetField("_theRuntime",
BindingFlags.NonPublic | BindingFlags.Static);
// set app path property value
FieldInfo appPath = field.GetValue(null).GetType().GetField("_appDomainAppPath",
BindingFlags.NonPublic | BindingFlags.Instance);
appPath.SetValue(field.GetValue(null), "c:\\test\\ApplicationPath");

// set codegen dir property value
FieldInfo codegenPath = field.GetValue(null).GetType().GetField("_codegenDir",
BindingFlags.NonPublic | BindingFlags.Instance);
codegenPath.SetValue(field.GetValue(null), "c:\\test\\ApplicationPath");
Requesting Gravatar... Sideout Feb 13, 2007 5:31 PM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
I almost got it working using Sergey's technique, but I still cannot MapPath. I get the exception:

System.Web.HttpException: The application relative virtual path '~/Data' cannot be made absolute, because the path to the application is not known.

at System.Web.VirtualPath.get_VirtualPathString()
at System.Web.HttpRequest.MapPath(VirtualPath virtualPath, VirtualPath baseVirtualDir, Boolean allowCrossAppMapping)

I tried to set _appDomainAppVPath using your technique but failed because it's a sealed VirtualPath class and not just a string

FieldInfo field = typeof(HttpRuntime).GetField("_theRuntime",
BindingFlags.NonPublic | BindingFlags.Static);
FieldInfo appVPath = field.GetValue(null).GetType().GetField("_appDomainAppVPath",
BindingFlags.NonPublic | BindingFlags.Instance);
appVPath.SetValue(field.GetValue(null), vPath);

You got yours working somehow without that though? My HttpContext.Current.Request.ApplicationPath is null probably because I don't have the AppVPath as well. All the other Console.WriteLine's print out proper values though.
Requesting Gravatar... Michael Freidgeim Apr 13, 2007 12:19 AM
# re: Print button
Your "Print" link prints pages with partially trimmed text on the right. It wiil be better to fix it or hide Print button at all.
Requesting Gravatar... Exploded Clown May 01, 2007 9:24 AM
# Simulating HttpContext with Sessions and POSTs for ASP.NET
Here&amp;#8217;s a little something I&amp;#8217;ve been hacking together at work, so that we can do some unit testing on my current project. It&amp;#8217;s largely based on this post by Phil Haack, but adds support for using Sessions (by assigning to the Session p...
Requesting Gravatar... multitao May 01, 2007 12:27 PM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
i am interested in how to simulate httpcontext with post input data. please clarify. thanks
Requesting Gravatar... Chris May May 28, 2007 12:26 PM
# Simulating HttpContext
Requesting Gravatar... Chris May Jun 03, 2007 2:05 PM
# Simulating HttpContext
Requesting Gravatar... you've been HAACKED Jun 19, 2007 11:26 PM
# Unit Tests Web Code Without A Web Server Using HttpSimulator
Unit Tests Web Code Without A Web Server Using HttpSimulator
Requesting Gravatar... Glenn Belardo Oct 10, 2007 3:49 AM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
This is a great approach but I think this will not work. HttpContext.Current is a read-only property so we cannot assign a new instance of HttpContext to it.
Requesting Gravatar... Haacked Oct 10, 2007 9:41 AM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
@Glenn I've been using this code for over a year. You telling me it doesn't work? Shoot!
Requesting Gravatar... Experiments In Writing Oct 14, 2007 6:12 PM
# The New ASP.NET Framework
Requesting Gravatar... Glenn Belardo Oct 15, 2007 8:52 PM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
Hi Haacked,

I was actually trying this with the code below:

Thread.GetDomain().SetData(".appPath", "C:\\testfolder\\Codes\\")
Thread.GetDomain().SetData(".hostingVirtualPath", "http://localhost")
Thread.GetDomain().SetData(".hostingInstallDir", HttpRuntime.AspInstallDirectory)
Dim tw As TextWriter = New StringWriter()
Dim wr As HttpWorkerRequest = New SimpleWorkerRequest("default.aspx", "", tw)
HttpContext.Current = New HttpContext(wr)

But it seems that it give me a build error that says HttpContext.Current property is a read-only. So I thought that it is not possible. I'll appreciate if you could tell me how come I got this error. Is there something that I am missing here?
Requesting Gravatar... Jai Feb 12, 2008 7:52 AM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
Hi,
I was successful creating a HttpContext but not HttpContext.Current.Profile since it is read-only. My application code makes use of Profile and the NUnit test fails because
"Property or indexer 'System.Web.HttpContext.Profile' cannot be assigned to -- it is read only". Any help here?

Thanks in advance.
Jai
Requesting Gravatar... Golf Shot Tips Jun 09, 2008 7:25 AM
# re: Simulating Http Context For Unit Tests Without Using Cassini nor IIS
Thanks Phil,

I have been fighting with my unit tests for a while now trying to simulate a FormsAuthentication session and kept running into the HttpContext for storing the cookie. This is a big help.

What do you have to say?

(will show your gravatar)
Please add 1 and 3 and type the answer here: