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.

4

2 回答 2

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.

于 2010-12-10T18:21:03.867 回答
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);
    }
}
于 2010-12-10T17:21:23.237 回答