Wednesday, 17 December 2008

Mocking the QueryString collection in ASP.NET

One of the hardest parts of building testable web applications using ASP.NET is the HttpContext object, which encapsulates access to the HTTP request and response, server state like the Session and Application objects, and ASP.NET's implementation of various other bits of the HTTP specification.

HttpContext has a God complex. It's all-seeing, all-knowing, ever-present, and most WebForms apps just call HttpContext.Current and work with whatever comes back. This approach really doesn't lend itself to test-driven designs, though, so the ASP.NET MVC team have implemented a collection of virtual base classes - HttpContextBase, HttpRequestBase, etc. - which gives us the ability to isolate elements of the HttpContext for testing purposes, either using a mocking framework or by writing our own test classes that inherit from those base classes. On the whole, this approach works fairly well - especially once you start explicitly passing an HttpContextBase into your controllers instead of letting them run amok with HttpContext.Current - but there's still some legacy implementation details inherited from ASP.NET that can cause a bit of confusion with your isolation tests.

In ASP.NET - both MVC and WebForms - the QueryString property of the HttpContext.Request claims to be a NameValueCollection. It isn't - which becomes immediately apparent if you're trying to test a controller method that handles IIS 404 errors. In classic mode, IIS will invoke a custom error handler as follows. Let's say you've mapped 404 errors to /MyMvcApp/Error/NotFound - where MyMvcApp is a virtual directory containing an ASP.NET MVC application, which contains an ErrorController with a NotFound() method.

image

When your browser requests http://myserver/page/is/not/here.aspx; IIS doesn't find anything, so it invokes your configured handler by effectively requesting the following URL:

http://myserver/MyMvcApp/Error/NotFound?404;http://myserver:80 /page/is/not/here.aspx

Notice that there's no key/value pairs in that query string. The code in my controller that parses it is using HttpContext.Request.QueryString.ToString() to extract the raw query string - but here's where it gets a bit weird. The framework claims that Request.QueryString is a NameValueCollection, but at runtime, it's actually a System.Web.HttpValueCollection. The difference is significant because HttpValueCollection.ToString() returns the URL-encoded raw query string, but NameValueCollection.ToString() returns the default Object.ToString() result - in this case "System.Collections.Specialized.NameValueCollection" - which really isn't much use to our URL parsing code.

So - to test our parsing code, we need our mock to return an HttpValueCollection. Problem is - this class is internal, so we can't see it or create new instances of it. The trick is to use System.Web.HttpUtility.ParseQueryString(), which will take the raw query string and return something that claims to be a NameValueCollection but is actually an HttpValueCollection. Pass in the URL you need to test, and it'll give you back a querystring object you can pass into your tests.

Putting it all together gives us something along these lines - this is using NUnit and Moq, but the query string technique should work with any test framework.

[Test]
public void Verify_Page_Is_Parsed_Correctly_From_IIS_Error_String() {

	// Here, we inject a test query string similar to that created
	// by the IIS custom error handling system.
	var iisQueryString = "404;http://myserver:80/i/like/chutney.html";
	var testQueryString = HttpUtility.ParseQueryString(iisQueryString);

	Mock<HttpRequestBase> request = new Mock<HttpRequestBase>();
	request.ExpectGet(req => req.QueryString).Returns(testQueryString);

	Mock<HttpContextBase> context = new Mock<HttpContextBase>();
	context.Expect(ctx => ctx.Request).Returns(request.Object);

	// Note that we're injecting an HttpContextBase into ErrorController
	// In the real app, this dependency is resolved using Castle Windsor.
	ErrorController controller = new ErrorController(context.Object);

	ActionResult result = controller.NotFound();

	// TODO: inspect ActionResult to check it's looked up the requested page
	// or whatever other behaviour we're expecting.
}

3 comments:

Mike Hadlow said...

Hi Dylan,

Great post. One of those 'I did not know that' ones :)

Not entirely relevant, but related, I've been experimenting with injecting an IHttpContextService.

http://mikehadlow.blogspot.com/2008/08/taking-httpcontext-out-of-mvc-framework.html

Dylan said...

I know - I've actually ended up using your interface wrapper approach since, although you can mock and inject HttpContextBase quite easily, I discovered after putting together the tests that getting Windsor to use new HttpContextWrapper(HttpContext.Current) is harder than it looks... any ideas?

Anthony McKinney said...

Thanks for this. Saved me a lot of time and headache.