tags:

views:

905

answers:

1

I'm attempting to unit test some code using NUnit. I have a method:

    public static string RenderRoute(HttpContextBase context, RouteValueDictionary values)
    {
        var routeData = new RouteData();
        foreach (var kvp in values)
        {
            routeData.Values.Add(kvp.Key, kvp.Value);
        }

        string controllerName = routeData.GetRequiredString("controller");
        var requestContext = new RequestContext(context, routeData);
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
        IController controller = factory.CreateController(requestContext, controllerName);

        var ActionInvoker = new ControllerActionInvoker();
        var controllerContext = new ControllerContext(requestContext, (ControllerBase)controller);
        ((ControllerBase)controller).ControllerContext = controllerContext;

        string actionName = routeData.GetRequiredString("action");

        Action action = delegate { ActionInvoker.InvokeAction(controllerContext, actionName); };

        return new BlockRenderer(context).Capture(action);
    }

My default controllerfactory is a StructureMap controller factory from MvcContrib. I'm also using the MvcMockHelpers from MvcContrib to help me mock the HttpContextBase.

The controller I am attempting to test calls the above RenderRoute method and blows up at:

IController controller = factory.CreateController(requestContext, controllerName);

With the error:

Controllers.WidgetControllerTests.CanCreateWidgetOnPage: System.Web.HttpException : The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception. ----> System.TypeInitializationException : The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception. ----> System.NullReferenceException : Object reference not set to an instance of an object.

I'm fairly new to unit testing/mocking and it's a possibility I'm not seeing something simple.

Here is the test I'm currently running:

    [Test]
    public void Test()
    {
        HttpContextBase context = MvcMockHelpers.DynamicHttpContextBase();
        string s = RenderExtensions.RenderAction<HomeController>(context, a => a.About());

        Console.WriteLine(s);
        Assert.IsNotNullOrEmpty(s);
    }

Any help would be appreciated.

I have simplified the problem down to this simple unit test:

    [Test]
    public void Test2()
    {
        HttpContextBase context = MvcMockHelpers.DynamicHttpContextBase();
        var routeData = new RouteData();
        routeData.Values.Add("Controller", "Home");
        routeData.Values.Add("Action", "About");


        string controllerName = routeData.GetRequiredString("controller");
        var requestContext = new RequestContext(context, routeData);
        IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
        IController controller = factory.CreateController(requestContext, controllerName);

        Assert.IsNotNull(controller);
    }
+2  A: 

I ran into this same issue when attempting to unit test a controller factory I wrote.

The issue appears to come from the ControllerTypeCache attempting to iterate through all associated assemblies on first invocation and uses BuildManager in doing this. The DefaultControllerFactory looks to be pretty extensible in this by using a BuildManager property to interact with an instance instead of directly being coupled but unfortunantely the property is marked internal. The MVC framework unit tests are able to access the internals of the MVC assembly unlike the rest of us.

After looking at how MVCContrib unit tests their controller factories I found they are using an extension method helper that overrides the controller cache using reflection to access a private property.

using System;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;

public static class ControllerFactoryTestExtension
{
    private static readonly PropertyInfo _typeCacheProperty;
    private static readonly FieldInfo _cacheField;

    static ControllerFactoryTestExtension()
    {
        _typeCacheProperty = typeof(DefaultControllerFactory).GetProperty("ControllerTypeCache", BindingFlags.Instance | BindingFlags.NonPublic);
        _cacheField = _typeCacheProperty.PropertyType.GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance);
    }

    /// <summary>
    /// Replaces the cache field of a the DefaultControllerFactory's ControllerTypeCache.
    /// This ensures that only the specified controller types will be searched when instantiating a controller.
    /// As the ControllerTypeCache is internal, this uses some reflection hackery.
    /// </summary>
    public static void InitializeWithControllerTypes(this IControllerFactory factory, params Type[] controllerTypes)
    {
        var cache = controllerTypes
            .GroupBy(t => t.Name.Substring(0, t.Name.Length - "Controller".Length), StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.ToLookup(t => t.Namespace ?? string.Empty, StringComparer.OrdinalIgnoreCase), StringComparer.OrdinalIgnoreCase);

        var buildManager = _typeCacheProperty.GetValue(factory, null);
        _cacheField.SetValue(buildManager, cache);
    }
}

After adding that to my unit test project I was able to add my own MockController type to the controller type cache using controllerFactory.InitializeWithControllerTypes(new[] {typeof(MockController)});

John Downey