Big picture thoughts on software and other topics

December 21, 2007

ASP.NET MVC - Simplified Controller Tests

by Brian Donahue

While ASP.NET MVCBroken Link: http://asp.net/downloads/3.5-extensions/ is definitely on the right track for improved testability of ASP.NET web applications, the CTP still leaves a fair amount to be desired when it comes to testing and mocking.  Phil HaackBroken Link: http://www.haacked.com/ provides some useful code for setting up your controllers with a mocked IHttpContextBroken Link: http://haacked.com/archive/2007/11/05/rhino-mocks-extension-methods-mvc-crazy-delicious.aspx, but I didn’t really agree with his decision to add an extension method onto the RhinoMocks MockRepository object.  To me, it seemed like this was an MVC concern, and the extension should be on the Controller object itself. 

I shuffled some things around, and added a quick fix for testing basic routing.  Notice I hard coded routes in this implementation, for better/different router support you’d need to pass in your RouteData that had been created with the correct routing for your app.  This is something I will need eventually, as my routing scenarios will soon go beyond simple [controller]/[action].  Of primary use is the CreateMockIHttpContext method which is Phil’s code.  You can create your own PrepareForTest extension method which delegates to the CreateMockIHttpContext method for creating the Controller’s Context.

public static class ControllerTestExtensions
{
// This is the important stuff that I "borrowed" from Phil
private static IHttpContext CreateMockIHttpContext(MockRepository mockery)
{
IHttpContext context = mockery.DynamicMock<IHttpContext>();
IHttpRequest request = mockery.DynamicMock<IHttpRequest>();
IHttpResponse response = mockery.DynamicMock<IHttpResponse>();
IHttpSessionState session = mockery.DynamicMock<IHttpSessionState>();
IHttpServerUtility server = mockery.DynamicMock<IHttpServerUtility>();

SetupResult.For(context.Request).Return(request);
SetupResult.For(context.Response).Return(response);
SetupResult.For(context.Session).Return(session);
SetupResult.For(context.Server).Return(server);

mockery.Replay(context);
return context;
}

// This is the method I call to prepare my controllers. You may need to tweak this for your routing scenarios or other concerns
public static void PrepareForTest(this Controller controller, string controllerName, string actionName, MockRepository mockery)
{
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]",
RouteHandler = typeof(MvcRouteHandler)
});

RouteData routeData = new RouteData();
routeData.Values.Add("Action", actionName);
routeData.Values.Add("Controller", controllerName);
controller.ControllerContext = new ControllerContext(CreateMockIHttpContext(mockery), routeData, controller);
SetupResult.For(controller.ControllerContext.HttpContext.Request.ApplicationPath).Return("/");

}
}


The above code was enough for my current needs, and allowed me to write tests like this:

[Test]
public void Should_call_user_service_to_create_user_and_redirect_to_login_page()
{
MockRepository mockery = new MockRepository();

IUserService service = mockery.CreateMock<IUserService>();
AccountController controller = CreateSUT(service);
controller.PrepareForTest("Account", "Create", mockery);

IHttpContext mockContext = controller.ControllerContext.HttpContext;

using (mockery.Record())
{
service.CreateUser(new NewUserDTO());
LastCall.IgnoreArguments();

SetupResult.For(mockContext.Request.Form).Return(new NameValueCollection());
// This is the part I like - I expect the Response to be a redirect to this path
// This is triggered in the Controller action by using the RedirectTo("Index", "Login") method
mockContext.Response.Redirect("/Login/Index");
}

using (mockery.Playback())
{
controller.Create();
}
}


Here’s the action code that makes the above pass:

[ControllerAction]
public void Create()
{
NewUserDTO dto = new NewUserDTO();
dto.UpdateFrom(Request.Form);
service.CreateUser(dto);
RedirectToAction("Index", "Login");
}


Of course there is no validation or any other complex scenario handling, but I haven’t written those tests yet!

I like the direction this is going, but it’s not nearly finished.  Different routing scenarios and other edge cases will require some refactoring.  If it gets to a point where I’m really happy with it, I’ll submit it to the MVC.ContribBroken Link: http://www.mvccontrib.org/ project.

Now, I’m messing with a workaround for testing RenderView calls in actions.  Currently this call is tightly coupled to some real context objects that don’t exist in xUnit land.  I may post my workaround, but it is also imperfect!