<< ASP.NET MVC - Simplified Controller Tests | Home | ASP.NET MVC A-Ha Moment: TempData == Flash >>

ASP.NET MVC - RenderView() Testing Workaround

posted @ Friday, December 21, 2007 6:49 PM

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!

Comments

  1. Haacked

    Posted on: 12/29/2007 3:09 AM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    I think the big irony is that people cringe at testing a subclass of the class under test, but they don't cringe at this (where SomeClass is a concrete class or an abstract class).

    SomeClass instance = mockery.PartialMock<SomeClass>();
    SetupResult.For(instance.SomeMethod()).Return(123);

    The thing is, what is happening under the hood? In principle, pretty much the same thing, only automatically! The part to cringe at is perhaps having a tiny bit of code for the test outside of the test.

    But in that case, we should cringe at any helper methods used in tests, such as the handy extension methods.

    We will change things so that that the test-specific subclass approach isn't necessary for testing controllers. However, I still don't get why it's so "cringeworthy".

  2. Brian Donahue

    Posted on: 12/29/2007 3:12 PM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    I agree that the difference is subtle. To me, the partial mock approach feels more natural because I look at the "untestable" method as a blackbox, or integration point, and I just will assume that it behaves appropriately. So, I want to just set an expectation that this method will be called with the right parameters and/or return what I expect. Actually overriding the method and setting a property and testing that property feels to me like you are just testing your override which isn't ever going to be executed in your real app. And yes the mocked method will never be called either, but to me it seems like a more clear way of saying "I can't control this method, but here is what I expect to happen" versus saying "I can't control this method so I will have it do something it doesn't real do and test that."

    Can't wait for the next CTP! I actually decided to not use MVC on a small client project because of some of these testability issues - not blaming the CTP, I knew that was part of the gamble, but I wanted to give it a shot! I'll write a post on that soon.

  3. Byron Walker

    Posted on: 1/7/2008 5:29 PM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    I too am not really a fan of the tst subclass pattern, however, I do believe the pattern to be effective.

    The Mvc is a fantastic piece of work, and am very happy to be building applications with it.

    I would like to see a few more interfaces. The testibility isn't quite as easy as I'd expected (understood its only a CTP). Eg, I know an instance of ViewContext can be created, but if it had an inteface, I could write so much less code in my tests.

    Keep up the good work Phil Haack and all invloved. I look forward to seeing future releases.

  4. Brian Donahue

    Posted on: 1/8/2008 10:35 AM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    Hi Byron,

    I agree. Hopefully my own particular taste about test patterns is not being interpreted as a criticism of the MVC framework - I have been geeked up about it since the Austin ALT.NET conference, and am still very excited that it's being created at all, let alone with direction from people like Phil, ScottGu, Rob Conery, et al. I just had to make a decision on a small project with a tight timeline that I'd rather use Monorail, with which I am comfortable with the testing process, rather than lose any time figuring out alternate test approaches for this CTP.

    I will certainly be digging back into the CTP (whether a new release is out or not) as soon as I have time again.

  5. Nate

    Posted on: 1/8/2008 11:07 AM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    So, for me the question is, knowing that this issue will be resolved in the next CTP release, which method will involve the least amount of refactoring once that release is dropped?

  6. Brian Donahue

    Posted on: 1/8/2008 1:45 PM

    # re: ASP.NET MVC - RenderView() Testing Workaround

    @Nate

    That is an interesting and valid question... From what I've seen, I don't think either approach has a clear advantage in refactoring. Either way, you are going to end up adding the same mocking setup code to mock the newly "mockable" stuff. Then you'll have some clean up. One advantage to Phil's approach is that the cleanup will *only* be in his tests, whereas with my approach I'd probably want to revert the controllers to extend Controller and not TestableController. I wouldn't *have* to do this, as there is no harm done by having it there, but it would be serving no purpose and just be noise, so I would opt to do so. It would be a simple search/replace and removal of the unneeded class, but if touching application code to refactor your tests smells to you, and Test SubClasses, don't (as much, at least)... well, you have your answer! :)

Your comment:



 (will not be displayed)


  Please add 5 and 1 and type the answer here: