Monday 1 December 2008

Unit Testing the UpdateModel method in ASP.NET MVC by Faking the Controller Context

One thing that’s not immediately obvious is that the UpdateModel method in ASP.NET MVC Beta requires a controller context to work.  In fact if you canll UpdateModel without controller context then you’re going to get an error like this:

threw exception:  System.ArgumentNullException: Value cannot be null.  Parameter name: controllerContext.

A quick search found that you need to fake the controller context to be able to unit test with UpdateModel, something Scott Gu doesn’t cover in his blog.  Never mind, Scott Hanselman to the rescue!  You will need to fake your controller context to get this to work correctly.  On Scott Hansleman’s blog he creates a MvcMockHelper class designed to mock certain aspects of the MVC environment, including the controller context.

This is the MvcMockHelpers class from Scott’s blog.

using System;
using System.Web;
using Rhino.Mocks;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Specialized;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace UnitTests
{
    public static class MvcMockHelpers
    {
        public static HttpContextBase FakeHttpContext(this MockRepository mocks)
        {
            HttpContextBase context = mocks.PartialMock<httpcontextbase>();
            HttpRequestBase request = mocks.PartialMock<httprequestbase>();
            HttpResponseBase response = mocks.PartialMock<httpresponsebase>();
            HttpSessionStateBase session = mocks.PartialMock<httpsessionstatebase>();
            HttpServerUtilityBase server = mocks.PartialMock<httpserverutilitybase>();
 
            SetupResult.For(context.Request).Return(request);
            SetupResult.For(context.Response).Return(response);
            SetupResult.For(context.Session).Return(session);
            SetupResult.For(context.Server).Return(server);
 
            mocks.Replay(context);
            return context;
        }
 
        public static HttpContextBase FakeHttpContext(this MockRepository mocks, string url)
        {
            HttpContextBase context = FakeHttpContext(mocks);
            context.Request.SetupRequestUrl(url);
            return context;
        }
 
        public static void SetFakeControllerContext(this MockRepository mocks, Controller controller)
        {
            var httpContext = mocks.FakeHttpContext();
            ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);
            controller.ControllerContext = context;
        }
 
        static string GetUrlFileName(string url)
        {
            if (url.Contains("?"))
                return url.Substring(0, url.IndexOf("?"));
            else
                return url;
        }
 
        static NameValueCollection GetQueryStringParameters(string url)
        {
            if (url.Contains("?"))
            {
                NameValueCollection parameters = new NameValueCollection();
 
                string[] parts = url.Split("?".ToCharArray());
                string[] keys = parts[1].Split("&".ToCharArray());
 
                foreach (string key in keys)
                {
                    string[] part = key.Split("=".ToCharArray());
                    parameters.Add(part[0], part[1]);
                }
 
                return parameters;
            }
            else
            {
                return null;
            }
        }
 
        public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod)
        {
            SetupResult.For(request.HttpMethod).Return(httpMethod);
        }
 
        public static void SetupRequestUrl(this HttpRequestBase request, string url)
        {
            if (url == null)
                throw new ArgumentNullException("url");
 
            if (!url.StartsWith("~/"))
                throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\".");
 
            SetupResult.For(request.QueryString).Return(GetQueryStringParameters(url));
            SetupResult.For(request.AppRelativeCurrentExecutionFilePath).Return(GetUrlFileName(url));
            SetupResult.For(request.PathInfo).Return(string.Empty);
        }
       
    }
}

Now all I need to do is

_Mocks = new MockRepository();
_ItemRepository = _Mocks.StrictMock<IItemRepository>();
 
SetupTestData(_Mocks, _ItemRepository);
_Target = new StockItemMasterController(_ItemRepository);
 
MvcMockHelpers.SetFakeControllerContext(_Mocks, _Target);

Set the controller context to the faked context and my tests will all start magically working.

2 comments:

thangchung said...

Would you describe it for details? I still un-clear when you wrote the Test method and the method that you set data for test. I very interested in it, because I'm working in FakeHttpContext's Scott. Thanks

Odd said...

At this point I'd consider upgrading to ASP.NET MVC 2 instead of worrying too much about faking the context. MAV2 makes it much easier by inheriting from a mockable base context.