This is how ASP.NET MVC controller actions should be unit tested
I'm upgrading my partywithpalermo.com website for the MVP Summit party, and I'm retrofitting my tests to use the March CTP of the MVC Framework. I have the following action that I need to unit test:
public class MainController : ControllerBase
{
private readonly IAttendeesRepository _repository;
public MainController(IAttendeesRepository repository, IViewEngine viewEngine)
{
_repository = repository;
ViewEngine = viewEngine;
}
public void Register(string name, string website, string comment)
{
var attendee = new Attendee(name, website, comment);
_repository.SaveNewAttendee(attendee);
RenderView("confirm", attendee);
}
}
Note the explicit dependencies on IAttendeeRepository and IViewEngine. That means that I'll be interacting with those two dependencies in this controller. Here is my unit test (this passes, by the way):
[Test]
public void ShouldSaveAttendee()
{
var repository = new FakeRepository();
var mockViewEngine = new MockViewEngine();
var controller = new MainController(repository, mockViewEngine);
controller.ControllerContext = new ControllerContext(new RequestContext(new HttpContextStub(), new RouteData()), controller);
controller.Register("Jeffrey.',", "http://www.jeffreypalermo.com?=&@%20", "this comment!?,'.");
Attendee attendee = repository.SavedAttendee;
Assert.That(attendee.Name, Is.EqualTo("Jeffrey.',"));
Assert.That(attendee.Website, Is.EqualTo("http://www.jeffreypalermo.com?=&@%20"));
Assert.That(attendee.Comment, Is.EqualTo("this comment!?,'."));
Assert.That(mockViewEngine.ActualViewContext.ViewName, Is.EqualTo("confirm"));
Assert.That(mockViewEngine.ActualViewContext.ViewData, Is.EqualTo(attendee));
}
private class HttpContextStub : HttpContextBase
{
}
This is a pretty straightforward unit test except for the line before calling the Register() method. I have to use setter-injection to set a stubbed ControllerContext. If I don't, the Register() method will bomb with an exception when the ViewContext is created, and the code tries to get an HttpContextBase off of the ControllerContext. It'll throw a NullReferenceException. My code doesn't really care about the ControllerContext or what is in it, but because of how the code is structured, I must use setter injection to break this dependency. Note that testability is next up on the list of things to do for the MVC Framework team.
Preview2 (March CTP) was all about getting the routing engine out into its own assembly so that it can be used separate from MVC controllers. Also, the MVC Framework is "binnable". You can xcopy deploy it. There is plenty of time to fix these things, and the team is working on it. You can also be sure that I'll keep raising the issue because I've been test-driving code for three years, and it's instinct now to see what is easy and frictionless and separate it from code that is harder to test than it should be. Overall, pretty good for a CTP.
The following is what I would like to write. The following is how I would like my test to look. Notice just the absence of the ControllerContext line.
[TestFixture]
public class MainControllerTester
{
[Test]
public void ShouldSaveAttendee()
{
var repository = new FakeRepository();
var mockViewEngine = new MockViewEngine();
var controller = new MainController(repository, mockViewEngine);
controller.Register("Jeffrey", "http://www.jeffreypalermo.com", "this comment");
Attendee attendee = repository.SavedAttendee;
Assert.That(attendee.Name, Is.EqualTo("Jeffrey"));
Assert.That(attendee.Website, Is.EqualTo("http://www.jeffreypalermo.com"));
Assert.That(attendee.Comment, Is.EqualTo("this comment"));
Assert.That(mockViewEngine.ActualViewContext.ViewName, Is.EqualTo("confirm"));
Assert.That(mockViewEngine.ActualViewContext.ViewData, Is.EqualTo(attendee));
}
private class MockViewEngine : IViewEngine
{
public ViewContext ActualViewContext;
public void RenderView(ViewContext viewContext)
{
ActualViewContext = viewContext;
}
}
private class FakeRepository : IAttendeesRepository
{
public Attendee SavedAttendee;
public IEnumerable<Attendee> GetAttendees()
{
throw new NotImplementedException();
}
public void SaveNewAttendee(Attendee attendee)
{
SavedAttendee = attendee;
}
}
}
Note that my unit test is concerned with explicit dependencies and doesn't know or care that I'm making use of a PROTECTED method named "RenderView" inside my action. That detail doesn't matter because the interaction with the IViewEngine is what is important.
I also understand that I could use the Legacy code pattern of "Extract and Override Call" (Feathers, p348). Microsoft has already provided the extracted method, RenderView. I can override the call to break the dependency, but that pattern is meant to be used as a dependency-breaking technique with legacy code. If you haven't read Michael Feathers' book, Working Effectively with Legacy Code, you should order it right now. It talks about all kinds of dependency-breaking techniques in order to write unit tests on existing code. My goal in providing feedback to the MVC Framework team (and I provide feedback, believe me) is to have this framework be something that I would want to use. There is plenty of time to make the necessary changes, and the team is working hard.
I revised this post a bit after chatting on the phone with Scott Guthrie this evening. We'll see some testability improvements in the next drop, and the team is aiming to make drops around every 6 weeks or so.
Note: I'm playing around with using "var" for locals. Not sure if I like it yet. We'll see. No need to comment on the "vars". Comment on controllers, actions, and unit tests.