Register  
Friday, March 12, 2010
Blogs  


Latest Blogs
  
ActiReq Preview
  
Latest Articles
  
Archive
  
Featured Blogs
  
15

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.

image 

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”.

image

Once done, we can rebuild and run the test again. This time, what we get is what we expect.

image

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

 

Reblog this post [with Zemanta]

Post Rating

Comments

lukesayaw
# lukesayaw
Friday, January 23, 2009 6:53 PM
cant download the source code.
Anonymous User
# Anonymous User
Friday, January 23, 2009 9:30 PM
Unit testing in Asp.Net -- Without a Server!
Antonio Bello
# Antonio Bello
Saturday, January 24, 2009 1:19 AM
Hi lukesayaw,

what problem are you experiencing with download? I have just tried, and it works. Maybe are you trying to download without be logged in?

Ant.
Shakti Singh Dulawat
Saturday, January 24, 2009 3:06 AM
Unit testing is important before publishing any website, this article is really grateful for new user who want to learn about unit testing.
Thanks
Shakti Singh Dulawat
http://www.nextmvp.blogspot.com/
Thanigainathan
# Thanigainathan
Tuesday, January 27, 2009 1:53 AM
Hi There,

The article is really nice. It gives a brief Description about Domains, Process everything.

Thanks,
Thani
toto
# toto
Tuesday, September 15, 2009 6:51 AM
unable to register to download the source code. A got an error saying "unable to send you an email". Of course I provided a valid email adress.
Antonio Bello
# Antonio Bello
Saturday, September 26, 2009 9:41 AM
Ho toto,

I apologize, but it looks like the smtp server had some problems in the latest weeks, and I didn't notice that.

Now it should ok - so please try logging in again and ask for the verification code to be sent again. In case of any problem please write here again.
Nikolas
# Nikolas
Tuesday, September 29, 2009 7:11 AM
Hello, very useful article. With handlers all clear. But what about http modules, how can i test my custom module. What i have to do for this?
Nikolas
# Nikolas
Tuesday, September 29, 2009 8:05 AM
Oh, I got it! I trying to use app.config of assembly, but this config file wasn't caught by worker process, I guess. Then I have copied web.config to the /debug directory and everything works fine.
Arno
# Arno
Tuesday, December 29, 2009 10:16 AM
Great article, thanks.

Post Comment

Name (required)

Email (required)

Website

Enter the code shown above in the box below

 

stock icons

 

 

© 2006-2008 Developer's Corner - Powered by Elapsus   |  Privacy Statement  |  Terms Of Use