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: http://www.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: http://www.lostechies.com/blogs/chad_myers/MVCRequestTesting.zip

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Chad Myers

Chad Myers is the Director of Development for Dovetail Software, in Austin, TX, where he leads a premiere software team building complex enterprise software products. Chad is a .NET software developer specializing in enterprise software designs and architectures. He has over 12 years of software development experience and a proven track record of Agile, test-driven project leadership using both Microsoft and open source tools. He is a community leader who speaks at the Austin .NET User's Group, the ADNUG Code Camp, and participates in various development communities and open source projects.
This entry was posted in ASP.NET MVC, TDD. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://weblogs.asp.net/scottgu ScottGu

    I would typically recommend that your unit tests test the individual sub-components of your web application (routing, controllers, any custom filters you create, etc), and then only use an approach like above for end to end integration testing of everything (which should hopefully be far fewer tests).

    I’m not sure I understand why you need to implement your ExecuteMvcUrl method though – can’t you just send standard URLs to the host and verify that the correct HTML is coming back? I’m not sure you want/need those end-to-end tests to be aware of which controller is handling what (your unit tests for your routing logic should cover those).

    Potentially I’m missing the uber scenario though. Feel free to send me email (scottgu@microsoft.com) if you want to drill into it more and I’d be happy to help.

    Thanks,

    Scott

  • http://chadmyers.lostechies.com Chad Myers

    @ScottGu

    These are end-to-end ‘black box’ tests, not unit tests. I agree with you vis-a-vis unit tests and we do indeed do that (it’s still somewhat of a challenge due to the abundant use of the *Context classes — RoutContext, ViewContext, etc).

    The reason I’m doing this here (instead of hitting a server) is so that they can be run in an automated fashion without the need for special setup on the box (i.e. on an Continuous Integration server that may not even have IIS installed).

    Right now I can check out the code from my SVN repo, build it, and run the tests on an XP box without IIS installed.

    We have layered tests that overlap a little, but we try to test different things at the various levels so as to avoid the “break a 1,000 tests when you refactor” problem.

    ** Unit tests – <2s secs, requires no special setup
    ** Integration Tests - <1 minute, requires database, maybe test data, some config files
    ** View Tests - >1 minute usually, involves just the views with minimal HttpContext setup, no HttpRuntime/ApplicationHost involves, WatiN and IE (soon FireFox)
    ** Black box end-to-end – >1 minute, involves the code above, includes the whole ASP.NET pipeline including URL routing, etc.

    The black box tests are usually very specific about what they check and don’t involve a lot of HTML munging or big string comparison — check that XYZ exists, check that ABC is disabled, check that I get redirected to the login screen, etc.

  • http://hex.lostechies.com erichexter

    While I appreciate the ability to test on the fly without IIS I think you could get similar results with the vs2008 dev webserver. I am not sure I understand why you would spend the time to do string assertions for the black box end-to-end tests, when you already have your view tests, which could check the state of objects in the webbrowser. Are you trying to eliminate the need to test multipage transactions in your view tests? At the end of the day shouldn’t the end-to-end tests drive the browser not the webserver?

    Regardless of the motivation for these tests, I think it is pretty cool that you can do it.

  • http://anydiem.com Sean Scally

    @chadmyers

    I’d really like to see the contents of each of those four test types that you mention; both what determines what goes in each type, and some examples of each.

    There’s just not enough real-life examples on the web of such a breakdown in .NET projects.