In my latest project I needed to perform some unit and integration testing for an Http Handler I’ve been working on. It is a notification handler, called by an external source to notify my system that a status change occurred on a previously submitted request.
Accordingly to the test driven development discipline, each unit test must be self contained and must not depend from an external environment, for instance a web server. NUntAsp is a good tool, implemented as an extension to the NUnit framework, but the major drawback is that it requires a web server in order to work – this also means that tested assemblies must be deployed to the web server in order for the tests to be executed.
I’ve been looking for a self-contained solution to run my tests, but I haven’t found a suitable one for my needs. Fortunately I’ve got some good ideas from other blogs posts (some of them listed at bottom - I apologize for the ones I’ve missed to list, but I don’t remember their links), which drove me to the solution I’ve implemented and that I’m about to explain.
The unit under test
In order to write test cases, we first need something to test. Some TDD purists may object that we should write a test before, and then start with development. In our case we need to test a unit by writing tests, but in order to write tests we first need the unit being tested, as tests are based on a framework we have to build.
So, let’s start with a simple HttpHandler:
using System.Web;
namespace DevCorner.MyHttpHandler
{
public class MyHttpHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable { get { return true; } }
public void ProcessRequest(HttpContext context)
{
context.Response.Write(string.Format("Hello, {0}", context.Request["name"]));
}
#endregion
}
}
I presume I don’t need to explain what it does, right? :)
Along with the MyHttpHandler class we also need a MyHttpHandler.ashx file:
<%@ WebHandler Language="C#" Class="MyHttpHandler.MyHttpHandler"
CodeBehind="MyHttpHandler.cs" %>
The testing framework
Our goal is to implement a class with at least one method allowing us to request a specific asp.net page or handler, optionally passing a query string:
public sealed class AspnetTester
{
public string CreateRequest(string page, string query)
{
...
}
}
The class and its method should be responsible of creating an environment where an asp.net page can be loaded, executed, and its output collected and returned as a string. By looking at the MSDN documentation and thanks to this article, the best candidate for executing aspx pages is the SimpleWorkerRequest class, which:
Provides a simple implementation of the HttpWorkerRequest abstract class that can be used to host ASP.NET applications outside an Internet Information Services (IIS) application. You can employ SimpleWorkerRequest directly or extend it.
It looks like this is exactly what we need to run our pages, outside a web server environment. The SimpleWorkerRequest class comes with 2 constructors, one requiring an HttpContext plus other parameters, the other one requires 3 parameters:
- the page to load
- a query string
- a TextWriter capturing the output generated by the requested page
We’re going to use this constructor, implementing the CreateRequest method as follows:
public sealed class AspnetTester
{
public string CreateRequest(string page, string query)
{
StringWriter writer;
SimpleWorkerRequest worker;
writer = new StringWriter();
worker = new SimpleWorkerRequest(page, query, writer);
HttpRuntime.ProcessRequest(worker);
writer.Flush();
return writer.GetStringBuilder().ToString();
}
}
The actual asp.net page execution is performed by the HttpRuntime.ProcessRequest method. But there is a problem with the this code: how do we have to provide the page we want to load? Can it be a physical or a virtual path? If not, what else?
If we closely look at the SimpleWorkerRequest constructor description, it states that it must be used “when the target application domain has been created using the CreateApplicationHost method”. This method is a static member of the ApplicationHost class, which allows us to create an application domain for processing asp.net request outside of IIS. For a brief description of an application domain, read the corresponding box below.
.NET Application Domains
A .NET application domain is an isolated container for code and data, conceptually comparable to a process.
The difference is that an application domain belongs to a single process, it can run multiple applications, and a single process can host more than one application domain. Anyway, even within the same process, application domain are isolated each other. It can be said that the .net runtime uses application domains pretty much the same as the operating system uses processes.
Application domains require less resources than processes, so they are cheaper to instantiate and perfectly suited for windows applications managing a high number of .net applications, such as IIS. In fact IIS uses a single ASP.NET worker process, where all ASP.NET applications are loaded in separate application domains.
For more information about application domains, read the msdn documentation.
So we need to create an instance of AspnetTester class using CreateApplicationHost method. The best way to do this is using a static method, as follows:
public sealed class AspnetTester
{
public string CreateRequest(string page, string query)
{
StringWriter writer;
SimpleWorkerRequest worker;
writer = new StringWriter();
worker = new SimpleWorkerRequest(page, query, writer);
HttpRuntime.ProcessRequest(worker);
writer.Flush();
return writer.GetStringBuilder().ToString();
}
public static AspnetTester CreateHost(string virtualPath, string physicalDir)
{
return (AspnetTester)ApplicationHost.CreateApplicationHost(
typeof(AspnetTester), virtualPath, physicalDir);
}
}
Our CreateHost method requires 2 parameters, forwarded to the CreateApplicationHost:
- the virtual path of the application domain
- the physical path of the application domain where the aspx pages are located
We can use any name as the virtual path for our web application, for instance “/myApp”. As for the physical path, we can use Environment.CurrentDirectory, which returns the path of the current working directory, which is the /bin/Debug/ folder of the project where we have created and we’re going to run the tests.
It looks like we have everything to start writing our tests. So let’s start with a simple test which simply verifies whether the Http Handler returns any output.
using System;
using DevCorner.Testing.Framework.Aspnet;
using NUnit.Framework;
namespace DevCorner.MyHttpHandler.Tests
{
[TestFixture]
public class MyHttpHandlerTests
{
[Test]
public void VerifyHandlerOutputNotEmpty()
{
AspnetTester tester;
string output;
tester = AspnetTester.CreateHost("/myApp", Environment.CurrentDirectory);
output = tester.CreateRequest("MyHttpHandler.ashx", string.Empty);
Assert.IsNotEmpty(output);
}
}
}
But this test fails, with this error:
System.IO.FileNotFoundException: Could not load file or assembly 'DevCorner.Testing.Framework.Aspnet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
So it looks like the DevCorner.Testing.Framework.Aspnet assembly, where I put the AspnetTester class, is not found. By setting a breakpoint to line 16, I can see that Environment.CurrentDirectory points to the bin\Debug folder of the project containing the test (as expected), but the missing assembly is there. So you may be wondering: why isn’t the assembly found if it’s there? The reason is that every asp.net application looks for assemblies in the bin folder of the application root, and as far as I know there is no way to change this behavior. So in order for the tests to be run, a bin folder must be created and all involved assemblies must be copied to that folder. We should do that manually, but this would break our first goal of having tests self contained. The solution is making this process automatic.
Setting up an application environment
I won’t spend many words on how to implement an application environment : given a physical path (the source folder), it simply has to create a bin subfolder (the target folder) and copy all assemblies from the source to the target. At the end it must clean up by removing the assemblies from the bin folder and remove the bin folder itself. Here is the code:
public sealed class ApplicationEnvironment : IDisposable
{
private readonly Stack<string> _filesToRemove = new Stack<string>();
private readonly string _basePath;
private readonly string _binFolder;
public void Dispose()
{
Cleanup();
}
public ApplicationEnvironment(string basePath)
{
_basePath = basePath;
if (_basePath.EndsWith(@"\") == false)
_basePath += @"\";
_binFolder = _basePath + @"bin\";
if (Directory.Exists(_binFolder) == false)
Directory.CreateDirectory(_binFolder);
AddAssemblies();
}
private void AddAssemblies()
{
string[] files;
files = Directory.GetFiles(_basePath, "*.dll");
foreach (string file in files)
AddToBinFolder(file);
}
private void AddToBinFolder(string filename)
{
FileInfo file;
file = new FileInfo(filename);
file.CopyTo(_binFolder + file.Name, true);
_filesToRemove.Push(file.Name);
}
private void Cleanup()
{
while (_filesToRemove.Count > 0)
{
var filename = _filesToRemove.Pop();
File.Delete(_binFolder + filename);
}
if (Directory.Exists(_binFolder))
Directory.Delete(_binFolder);
}
}
Now we can rewrite our test as follows:
[Test]
public void VerifyHandlerOutputNotEmpty()
{
AspnetTester tester;
string basePath;
string output;
basePath = Environment.CurrentDirectory;
using (new ApplicationEnvironment(basePath))
{
tester = AspnetTester.CreateHost("/myApp", basePath);
output = tester.CreateRequest("MyHttpHandler.ashx", string.Empty);
}
Assert.IsNotEmpty(output);
}
Last minor fixes
Running the test as described above results in a new exception, informing us that the AspnetTester class is not serializable. So, let’s add the missing attribute and run test again.
What comes next is more interesting: now we have this exception:
System.NullReferenceException: Object reference not set to an instance of an object.
I honestly have to admit that this issue took long time to be solved – I’m just going to give you the solution rather than listing all things I’ve tried: the AspnetTester class must inherit from the MarshalByRefObject class. In fact, as stated in the MSDN documentation:
MarshalByRefObject is the base class for objects that communicate across application domain boundaries by exchanging messages using a proxy
and this is our case. After applying this simple change, the test succeeds.
The last test
Just to verify that what the output of the http handler under test is correctly caught, let’s write the last test proving that the framework works. First, some refactoring: the ApplicationEnvironment and AspnetTester don’t need to be instantiated for every test, so it’s better if we move their creation and disposal in methods executed once and not for every test – this can be easily done taking advantage of the TestFixtureSetUp and TestFixtureTearDown attributes.
[TestFixture]
public class MyHttpHandlerTests
{
private ApplicationEnvironment _applicationEnvironment;
private AspnetTester _tester;
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
string basePath;
basePath = Environment.CurrentDirectory;
_applicationEnvironment = new ApplicationEnvironment(basePath);
_tester = AspnetTester.CreateHost("/myApp", basePath);
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
_tester = null;
_applicationEnvironment.Dispose();
_applicationEnvironment = null;
}
[Test]
public void VerifyHandlerOutputNotEmpty()
{
string output;
output = _tester.CreateRequest("MyHttpHandler.ashx", string.Empty);
Assert.IsNotEmpty(output);
}
}
Now we can code our new test, which simply provides a parameter in the query string as expected by the Http Handler. The handler simply reads its value and outputs a hello message including the provided name
[Test]
public void ParseHandlerOutput()
{
string output;
output = _tester.CreateRequest("MyHttpHandler.ashx", "name=Antonio");
Assert.IsNotEmpty(output);
Assert.AreEqual("Hello, Antonio", output);
Console.Out.Write(output);
}
Let’s run the test and… wow, it failed, again – the output is not what we expected. In fact we have an asp.net error page stating that the resource cannot be found. And that’s correct, as the ashx file is not copied to the application root. This issue is quickly solved by selecting the MyHttpHandler.ashx file from the solution, and then from the properties box set the “Copy to Output Directory” value to “Copy if newer”.
Once done, we can rebuild and run the test again. This time, what we get is what we expect.
The source code for this article is available here.
References
MSDN - Application Domains Overview
Scott Hanselman’s blog - NUnit Unit Testing of ASP.NET Pages, Base Classes, Controls and other widgetry using Cassini
CodeProject - Using ASP.NET Runtime in Desktop Applications
NunitASP – ASP.NET Unit Testing framework