Friday, December 21, 2007
In my last post, I talked about testing controllers in the ASP.NET MVC CTP and showed some sample code for mocking context, and testing simple routing. Another tricky thing to test in Controllers is the RenderView method. In the CTP, RenderView relies on some contextual objects that aren’t present outside of a running web application. Therefore, if you call RenderView("ViewName", ViewData) in your controller action that you’re testing, you’ll get a Null Reference Exception.
Again, Phil Haack has offered some solutions using test-specific subclasses. Phil has used the subclass approach in tests before, and count me as one of the folks that cringe at this approach, which Phil acknowledges as being a matter of taste. To me, it just doesn’t feel right to override your class under test, and test your override.
So, without using a test-specific subclass, how do I know my Action is calling RenderView for the right view, and passing in the right ViewData? Because RenderView relies on contextual objects that my tests don’t have access to, I decided to treat it like a black box and just assume that if my Action called RenderView with the "right stuff," I’d assume that RenderView knew what it was doing. I decided I wanted to mock out the RenderView call, and just set an Expectation that it was called with the correct parameters. Problem. RenderView is is protected. So, I used a klugey subclass of my own. I created a TestableController that simply hides the protected RenderView and exposes a public virtual RenderView.
At least here, all my controllers can inherit from this class, and I can set Expectations in all tests. It’s not something I’ll have to re-implement to extend every Controller Type I write (HomeControllerTester:HomeController, ProductControllerTester:ProductController, etc), as Phil’s example would require. Here is what it looks like:
public class TestableController : Controller
{
new public virtual void RenderView(string viewName, object viewData)
{
base.RenderView(viewName, viewData);
}
}
Simple enough. In "real" scenarios, it just delegates to the base RenderView. But in test scenarios, I can mock it and set expectations like so:
[Test]
public void Should_display_notes_for_a_given_user()
{
INotesService service = mockery.DynamicMock<INotesService>();
int userId = 1;
List<NoteDTO> notes = new List<NoteDTO>();
// Note that here I am using a PartialMock which allows me to selectively mock RenderView
NotesController controller = mockery.PartialMock<NotesController>(service);
controller.PrepareForTest("Notes", "Index", mockery);
using (mockery.Record())
{
Expect.Call(service.GetNotesForUser(userId)).Return(notes);
controller.RenderView("Index", notes);
}
using (mockery.Playback())
{
controller.Index(userId);
}
}
So now I can be sure that at least RenderView is being called for the correct view, and being passed the correct ViewData. I can (hopefully) trust that the real implementation will do its job.
Good news: Phil mentions in his post that this will be fixed in the next CTP update, so I can just get rid of my TestableController hopefully at that point!
While ASP.NET MVC 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 Haack provides some useful code for setting up your controllers with a mocked IHttpContext, 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.Contrib 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!