December 21, 2007
ASP.NET MVC - RenderView() Testing Workaround
by Brian Donahue
In my last post, I talked about testing controllers in the ASP.NET MVCBroken Link: http://asp.net/downloads/3.5-extensions/ 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 HaackBroken Link: http://www.haacked.com/ has offered some solutions using test-specific subclassesBroken Link: http://www.haacked.com/archive/2007/12/09/writing-unit-tests-for-controller-actions.aspx. 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!