Hosting an entire ASP.NET MVC request for testing purposes


We’ve been playing around a lot with the ASP.NET MVC Preview 3 at Dovetail and trying to find various ways to test at various levels.  One of the levels we were toying with was doing an end-to-end test including URL routing, controller action, and view result rendering.  Unfortunately it’s still pretty complicated due to a lot of coupling in the bowels of the routing, controller invocation, and view rendering pieces but we were able to get an in-process host for fast-feedback testing without the need to fire up a browser or consult IIS.

The code and a few other things related this post can be found here: https://lostechies.com/blogs/chad_myers/MVCRequestTesting.zip

Hosting the ASP.NET Runtime

In order to process the first request, I found I had to do a number of things to prime the ASP.NET pump as well as ASP.NET MVC in order to process the first request. These steps include:

  1. Creating an application host in a separate AppDomain using ApplicationHost.CreateApplicationHost()
  2. Prime the ASP.NET engine with a bogus first request
  3. Execute the URL to a StringWriter and then return the result

Here’s a cheesy visualization that will hopefully illustrate things a little better:

Creating An Application Host and Initializing ASP.NET

First, you’ll need a class that will be created in the other app domain. In order to talk across app domain boundaries, the class will have to derive from System.MarshalByRefObject (heretofore known as MBRO).  For those not already familiar with MBRO’s, MBRO’s allow a class to be called from a remote application domain (including those in separate processes or even separate machines) and have the calls marshaled back to the object’s real physical location, executed, and then the results marshaled back to the caller.  The magic happens in the object’s home AppDomain, but things appear to be happening at the call site.  Nifty when you need such behavior, but it’s not without it’s own pitfalls (which are out of the scope of this article, unfortunately).

The humble TestASPAppHost object starts out looking like this (basic MBRO starter implementation):

   1: using System;
   2:
   3: namespace MVCRequestTesting.Web
   4: {
   5:     public class TestASPAppHost : MarshalByRefObject
   6:     {
   7:         public override object InitializeLifetimeService()
   8:         {
   9:             // This tells the CLR not to surreptitiously 
  10:             // destroy this object -- it's a singleton
  11:             // and will live for the life of the appdomain
  12:             return null;
  13:         }
  14:     }
  15: }

In order to start the ASP.NET stuff, you need to look at the System.Web.Hosting.ApplicationHost object and it’s CreateApplicationHost method.  All this does is create an instance of your object in a new app domain and return a wrapped object reference to you.  I like to encapsulate this stuff as a static ‘factory’-type method on the object itself to keep things neat and tidy for the caller, so I added a method called GetHost which takes the physical path to where the web root will be. In my example, I have two projects in a solution: The MVC web app project (MVCRequestTesting.Web) and the integration test project (MVCRequestTesting.IntegrationTesting).  So my physical path will the MVCRequestTesting.Web root folder.  I’ll need to access this path relatively from where the NUnit tests are actually being run which can get a bit tricky. More on that later. For right now, here’s what my GetHost method looks like:

 

   1: public static TestASPAppHost GetHost(string webRootPath)
   2: {
   3:     var host = (TestASPAppHost)ApplicationHost.CreateApplicationHost(
   4:                                    typeof(TestASPAppHost),
   5:                                    "/test",
   6:                                    webRootPath);
   7:
   8:     // Run a bogus request through the pipeline to wake up ASP.NET and initialize everything
   9:     host.InitASPNET();
  10:
  11:     return host;
  12: }
  13:
  14: public void InitASPNET()
  15: {
  16:     HttpRuntime.ProcessRequest(new SimpleWorkerRequest("/default.aspx", "", new StringWriter()));
  17: }

The ‘host’ variable actually lives in the other app domain. What we know as ‘host’ is actually a wrapped object reference. All calls to host.Foo will be marshaled to the other app domain, executed there, and the results marshaled back.

There’s one last thing we need to worry about: The Web path.  It must be relative from where the testing assembly is, but this can be tricky. When running in the debugger in Visual Studio, it’s going to be in the binDebug dir of the IntegrationTesting project. When run from the NUnit runner (console or GUI), it’s going to assembly shadow copying, so it’ll be in some funky path like c:userschadAppDataLocalDatablahblahblah.

It turns out that we don’t want to just use the Assembly.Location. We can use the Assembly.CodeBase. Unfortunately, CodeBase is in the URI format, so it doesn’t work with the System.IO.Path convenience methods. Yadda yadda. Anyhow, I wrote a convenience method that lets you specify the relative path from the testing assembly code base (i.e. binDebug) regardless of where it’s being run from:

   1: public static TestASPAppHost GetHostRelativeToAssemblyPath(string relativePath)
   2: {
   3:     string asmFilePath = new Uri(typeof(TestASPAppHost).Assembly.CodeBase).LocalPath;
   4:     string asmPath = Path.GetDirectoryName(asmFilePath);
   5:     string fullPath = Path.Combine(asmPath, relativePath);
   6:     fullPath = Path.GetFullPath(fullPath);
   7:
   8:     return GetHost(fullPath);
   9: }

So now it’s easy to get an app host set up and pointed to your web project folder regardless of the context in which the tests are being run. For example:

   1: var host = TestASPAppHost.GetHostRelativeToAssemblyPath(@"......MVCRequestTesting.Web");

Executing an MVC Request and Getting a Response

Now comes the fun part.  Here’s what I want my test to look something like this:

   1: var host = TestASPAppHost.GetHostRelativeToAssemblyPath(@"......MVCRequestTesting.Web");
   2: var htmlResult = host.ExecuteMvcUrl("Home/Index", "");
   3: Assert.That(htmlResult, Text.Contains("<h2>Welcome to ASP.NET MVC!</h2>"));

The last trick is to actually execute a request through the ASP.NET and MVC pipelines, get the resultant HTML and then be able to execute tests on it. So here’s the coup de grâce that enables all of this: Behold the ExecuteMvcUrl method:

   1: public string ExecuteMvcUrl(string url, string query)
   2: {
   3:     var writer = new StringWriter();
   4:     var request = new SimpleWorkerRequest(url, query, writer);
   5:     var context = HttpContext.Current = new HttpContext(request);
   6:     var contextBase = new HttpContextWrapper2(context);
   7:     var routeData = RouteTable.Routes.GetRouteData(contextBase);
   8:     var routeHandler = routeData.RouteHandler;
   9:     var requestContext = new RequestContext(contextBase, routeData);
  10:     var httpHandler = routeHandler.GetHttpHandler(requestContext);
  11:     httpHandler.ProcessRequest(context);
  12:     context.Response.End();
  13:     writer.Flush();
  14:     return writer.GetStringBuilder().ToString();
  15: }

Awesome isn’t it?  No? Not really? Yeah, it made me feel a little dirty, too. Unfortunately there’s a lot of tight coupling going on all over the place, so it’s difficult to be able to go from request to HTML without setting up a bunch of things first.  The biggest culprit, unfortunately, is the routing/URL handling stuff. But, being tucked away in the corner of our testing host, it’s not too bad because it enables simple testing as demonstrated above.

Running the Test

Finally, I used TestDriven.NET to run the test (or you can use R#’s UnitRunner or the NUnit Gui/Console runner) and got:

   1: ------ Test started: Assembly: MVCRequestTesting.IntegrationTesting.dll ------
   2:
   3:
   4: 1 passed, 0 failed, 0 skipped, took 7.61 seconds.

The code and a few other things related this post can be found here: https://lostechies.com/blogs/chad_myers/MVCRequestTesting.zip

Neat Tricks with StructureMap