Unit Testing with ASP.NET MVC

I was involved with a new web application using ASP.NET MVC 4 and wanted to write some unit tests right from the start. Users of this application authenticates using client certificates (stored on smart cards), and I was a little worried about mocking the http context. That turned out to be relatively easy, since Microsoft pulled themselves together and built MVC with unit testing in mind. Controllers have a property Context that you can use to inject your mocked context. The properties of the context, such as Request and Response, is of type RequestBase, ResponseBase etc, so you can inject your own mocked objects into the context . I found some helper code at Scott Hanselman’s blog.

The harder problem was the client certificate. Request.ClientCertificate returns a HttpClientCertificate instance, and HttpClientCertificate does not have a public constructor. So I had to turn to Moles from Microsoft Research (were using Visual Studio 2010).

Some important points on using Moles:

  1. To get a mole for a certain assembly (in my case System.Web) , right-click on the assembly under references in your unit test project and choose Add Moles Assembly. This will add a file *.moles to your project. This is an XML file with build action Moles that specifies what moles compiler should do. Now compile the project.
  2. To generate moles for mscorlib, right-click on the References folder.
  3. If you generate moles for the System assembly, you will probably get compilation errors. The solution is to edit System.moles and add only the namespaces you need, e.g.
    <Moles xmlns="http://schemas.microsoft.com/moles/2010/">
      <Assembly Name="System" />
      <StubGeneration>
        <Types>
          <Clear />
          <Add Namespace="System.Net.Mail" />
        </Types>
      </StubGeneration>
    </Moles>
    
  4. If a class does not have a public constructor, you can new up the corresponding Moles type. Se code below.
  5. You must tag your unit test methods with the following attribute to activate Moles: [HostType(“Moles”)]
  6. You must problably edit C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\Microsoft.Moles.VsHost.x86.exe.config and disable legacyCasPolicy.
  7. If you’re using TFS/MSBuild to build your solution and run your unit tests, you must install Moles on the build server.

Here is an example on how to create a fake certificate:

_controller = new MyController();
var httpClientCertificate = new MHttpClientCertificate
    {
        IsPresentGet = () => true,
        SubjectGet = () => "...",
        IssuerGet = () => "..."
    };
_controller.SetFakeControllerContext(httpClientCertificate);

And here is the complete controller context helper:

using System;
using System.Web;
using System.Collections.Specialized;
using System.Web.Moles;
using System.Web.Mvc;
using System.Web.Routing;
using Moq;

namespace UserAdmin.UnitTests
{
    public static class MvcMockHelpers
    {
        public static HttpContextBase FakeHttpContext(HttpClientCertificate httpClientCertificate)
        {
            var context = new Mock<HttpContextBase>();
            var request = new Mock<HttpRequestBase>();
            var response = new Mock<HttpResponseBase>();
            var session = new Mock<HttpSessionStateBase>();
            var server = new Mock<HttpServerUtilityBase>();

            request.Setup(req => req.ClientCertificate).Returns(httpClientCertificate);
            request.Setup(req => req.IsAuthenticated).Returns(true);

            context.Setup(ctx => ctx.Request).Returns(request.Object);
            context.Setup(ctx => ctx.Response).Returns(response.Object);
            context.Setup(ctx => ctx.Session).Returns(session.Object);
            context.Setup(ctx => ctx.Server).Returns(server.Object);

            return context.Object;
        }

        public static HttpContextBase FakeHttpContext(string url, HttpClientCertificate httpClientCertificate)
        {
            HttpContextBase context = FakeHttpContext(httpClientCertificate);
            context.Request.SetupRequestUrl(url);
            return context;
        }

        public static void SetFakeControllerContext(this Controller controller, HttpClientCertificate httpClientCertificate)
        {
            var httpContext = FakeHttpContext(httpClientCertificate);
            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)
        {
            Mock.Get(request)
                .Setup(req => req.HttpMethod)
                .Returns(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 \"~/\".");

            var mock = Mock.Get(request);

            mock.Setup(req => req.QueryString)
                .Returns(GetQueryStringParameters(url));
            mock.Setup(req => req.AppRelativeCurrentExecutionFilePath)
                .Returns(GetUrlFileName(url));
            mock.Setup(req => req.PathInfo)
                .Returns(string.Empty);
        }
    }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s