Saturday, March 8, 2008

Testing ASP.NET MVC using Typemock Isolator

Update: There is a much simpler version of these tests that does not even require any helper classes. You may want to look at that as well.

 

Scott Hanselman posted his notes and slides (and movies!) from his talk on the ASP.NET MVC framework, where he also mentioned and shows how he's write unit tests for the MVC framework in ASP.NET.

Scott has examples both in Moq and Rhino.Mocks on how to write these tests, so it should be no surprise we'd want to get into the game.

You can download a zip file that contains the Typemock version of the Mvc mock Helpers here. This version should work with the enterprise version of Typemock, but we'll make a community edition available soon.

Here is the full source code of the mocking helper class.

 

using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using TypeMock;

namespace Typemock.Mvc
{
static class MvcMockHelpers
{
public static void SetFakeContextOn(Controller controller)
{
HttpContextBase context = MvcMockHelpers.FakeHttpContext();
controller.ControllerContext = new ControllerContext(new RequestContext(context, new RouteData()), controller);
}

public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod)
{

using (var r = new RecordExpectations())
{
r.ExpectAndReturn(request.HttpMethod, 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 parameters = GetQueryStringParameters(url);
var fileName = GetUrlFileName(url);
using (var r = new RecordExpectations())
{
r.ExpectAndReturn(request.QueryString, parameters);
r.ExpectAndReturn(request.AppRelativeCurrentExecutionFilePath, fileName);
r.ExpectAndReturn(request.PathInfo, string.Empty);
}
}

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 HttpContextBase FakeHttpContext(string url)
{
HttpContextBase context = FakeHttpContext();
context.Request.SetupRequestUrl(url);
return context;
}
public static HttpContextBase FakeHttpContext()
{
HttpContextBase context = MockManager.MockObject<HttpContextBase>().Object;
HttpRequestBase request = MockManager.MockObject<HttpRequestBase>().Object;
HttpResponseBase response = MockManager.MockObject<HttpResponseBase>().Object;
HttpSessionStateBase sessionState = MockManager.MockObject<HttpSessionStateBase>().Object;
HttpServerUtilityBase serverUtility = MockManager.MockObject<HttpServerUtilityBase>().Object;
using (var r = new RecordExpectations())
{
r.DefaultBehavior.RepeatAlways();
r.ExpectAndReturn(context.Response, response);
r.ExpectAndReturn(context.Request, request);
r.ExpectAndReturn(context.Session, sessionState);
r.ExpectAndReturn(context.Server, serverUtility);
}
return context;
}

}
}



And here is an example of how you can write tests with it:




 



using System.Collections;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using MvcApplication1;
using MvcApplication1.Controllers;
using NUnit.Framework;
using Typemock.Mvc;

namespace MvcAppTests
{
[TestFixture]
public class HomeControllerTests
{
[Test]
public void Hello_ShowsTheHelloView()
{
FakeViewEngine engine = new FakeViewEngine();
HomeController controller = new HomeController();
controller.ViewEngine=engine;
MvcMockHelpers.SetFakeContextOn(controller);

controller.Hello();
Assert.AreEqual("Hello",engine.CurrentViewContext.ViewName);

}

[Test]
public void RouteDefaultsWork()
{
var app = new GlobalApplication();
RouteCollection routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);
var context = MvcMockHelpers.FakeHttpContext();
context.Request.SetupRequestUrl("~/home");

var data = routes.GetRouteData(context);
Assert.AreEqual("home",data.Values["controller"]);
Assert.AreEqual("Index",data.Values["action"]);
}
}

internal class FakeViewEngine : IViewEngine
{
public ViewContext CurrentViewContext;
public void RenderView(ViewContext viewContext)
{
CurrentViewContext = viewContext;
}
}
}


2 comments:

Eric Hexter said...

Roy,
Do you want to contribute this to the MvcContrib project? http://mvccontrib.org

If you are cool with it, but do not want to get into another oss project post to this group and one of the contributors can roll it in.

http://groups.google.com/group/mvccontrib-discuss

Roy Osherove said...

Eric: Great idea!