2

I'm trying to unit test a controller that stuffs data into a ViewData. All our views require similar data (Customer info which is derived from the url). So instead of putting the call into every single controller method, the original developers chose to put this ViewData stuffing into the OnActionExecuting event.

Of course, when you invoke the controller's action from a unit test, OnActionExecuting doesn't fire. (Thanks MVC Team!)

So I tried creating a custom view engine and having it stuff the customer data into the controllerContext when the view is requested. This works fine in a browser, but my viewEngine is ignored when I run this test. No amount of ViewEngines.Add(new funkyViewEngine) has any effect.

  [TestMethod()]
            public void LoginTest()
            {
                ViewEngines.Engines.Clear();
                ViewEngines.Engines.Add(new FunkyViewEngine());

                UserController target = new UserController();
                target.SetStructureMap();  <--sets us to use the test repo

                target.ControllerContext.HttpContext = MVCHelpers.FakeHttpContext("https://customersubdomain.ourdomain.com");  <--moq magic
                var actual = target.Login();
                Assert.IsTrue(actual.GetType().IsAssignableFrom(typeof(System.Web.Mvc.ViewResult)));
                var  vr = actual as ViewResult;
                Assert.IsTrue(vr.ViewData.Community() != null);  <--"Community" should be set by viewengine
                Assert.IsTrue(vr.ViewData.Community().Subdomain == "customersubdomain.ourdomain");
                Assert.IsTrue(vr.ViewData.Community().CanRegister);
            }

Is there any hope here? How do I either 1) create a method that gets called on controller execution BOTH in the browser and the unit framework or 2) get the unit framework to invoke my view engine.

Code Silverback
  • 3,204
  • 5
  • 32
  • 39

2 Answers2

2

Sorry for your frustration. The reason why you are seeing OnActionExecuting not being called when you directly call your action method from the unit test is because that's not how things work in MVC.

The request gets executed via a "pipeline", which as far as this area is concerned consists of the ControllerActionInvoker. This class is responsible for:

  1. Finding the action method
  2. Invoking action filters' OnActionExecuting method (note: your controller class is also an action filter)
  3. Calling the action method itself
  4. Invoking the action filters' OnActionExecuted method
  5. Handling the result (e.g. finding the view and rendering it)

In your unit test you are directly invoking step 3. and skipping all the other steps. In a unit test, it is your responsibility to call any setup code required for your action to work.

However, this does not mean you should now write unit tests that use the ControllerActionInvoker to execute the entire pipeline. We (the MVC team) have already verified that all the pieces work together.

Instead, you should test your specific application code. In this case, you might consider having the following unit tests:

  1. A test that verifies that given some Url calling OnActionExecuting on your controller puts the right Customer object into ViewData
  2. A test that verifies that given some Customer object present in ViewData your action method returns the appropriate result

My last point is that you should keep the functionality in OnActionExecuting. A custom view engine is definetely the wrong place for it.

marcind
  • 52,944
  • 13
  • 125
  • 111
  • Yeah, the ViewEngine thing was a hack to get around the fact that this behavior makes all but trivial controllers totally untestable. I can put a public wrapper that invokes OnActionExecuting and OnActionExecuted for me and call it manually, but that doesn't help for my attributes like RoleFilter, SessionFilter, RequireHttps, etc. – Code Silverback Dec 10 '10 at 19:12
  • You should individually test the components that make up your application and trust :) that the framework will make everything work together. For filter attributes for example you could just use reflection to verify that the attribute is declared on your method. Mvc was designed to be testable and the only time when that's difficult is when you're trying to test the framework itself. When you run into issues like this it a good indicator that you might have to take a different approach. – marcind Dec 10 '10 at 19:29
  • That's still totally broken that I cannot use unit tests to verify that MyController.Action correctly redirects http to https and logs the user in. Those us trying to drag their teams kicking and screaming towards TDD are poorly served by the MVC team crippling our tests. – Code Silverback Dec 10 '10 at 19:37
  • My argument is that if you are trying to verify the end-to-end behavior it is no longer the domain of unit tests but the domain of functional tests. I also disagree with the statement about "MVC team crippling our tests" (perhaps because i'm part of the MVC team :). We've put a lot of effort into exposing numerous extensibility points. However, if you do want to test the end-to-end behavior of the framework itself you can use `ControllerActionInvoker` in a unit test. But I would argue that by doing so you are unnecessarily expending your efforts when they could be better spent elsewhere. – marcind Dec 10 '10 at 20:22
  • So Assert.IsTrue(MyController.Login() is typeof(RedirectResult)) isn't a unit test when I use [RequireHttps] but it *is* a unit test if I write the https detection by hand in the body of my method? That sounds like a religious disputation. The bottom line is: I can't do automated tests of my code without using the "you don't need to use this" ControllerActionInvoker. That's nuts. – Code Silverback Dec 10 '10 at 20:40
  • MVC is a framework written based on choices adhering to a particular design philosophy (some might even call it 'religion'). I see unit tests as tests that directly test code that *you* wrote. If the code is an attribute on the action method then you should test if the action method has that attribute. If it's manual redirection then you should test if the manual redirection works. Otherwise you are testing guarantees provided by the framework. E.g. we guarantee that if you have an action method with `[RequireHttps]` then the redirect will happen for non-https requests. We already tested that. – marcind Dec 10 '10 at 21:07
0

Not an answer you're probably looking for, but I'm using a custom MvcHandler to achieve the same goal (getting customer from URL in multi-tenant app). ViewEngine doesn't sound like a good place for this kind of logic to me...

My custom handler looks more or less like this:

public class AccountMvcHandler : MvcHandler
{
    public Account Account { get; private set; }

    protected override IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state)
    {
        return base.BeginProcessRequest(httpContext, callback, state);
    }

    protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
    {
        string accountName = this.RequestContext.RouteData.GetRequiredString("account");
        Account = ServiceFactory.GetService<ISecurityService>().GetAccount(accountName);

        return base.BeginProcessRequest(httpContext, callback, state);
    }
}
Jakub Konecki
  • 45,581
  • 7
  • 87
  • 126
  • Can you clarify a bit? The ViewEngine hack is just an ugly fallback to the lack of action wiring in the unit test approach. If there's a better way, I'd love to hear. – Code Silverback Dec 10 '10 at 17:25