4

I want to write a unit test that verifies my route registration and ControllerFactory so that given a specific URL, a specific controller will be created. Something like this:

Assert.UrlMapsToController("~/Home/Index",typeof(HomeController));

I've modified code taken from the book "Pro ASP.NET MVC 3 Framework", and it seems it would be perfect except that the ControllerFactory.CreateController() call throws an InvalidOperationException and says This method cannot be called during the application's pre-start initialization stage.

So then I downloaded the MVC source code and debugged into it, looking for the source of the problem. It originates from the ControllerFactory looking for all referenced assemblies - so that it can locate potential controllers. Somewhere in the CreateController call-stack, the specific trouble-maker call is this:

internal sealed class BuildManagerWrapper : IBuildManager {
    //...

    ICollection IBuildManager.GetReferencedAssemblies() {
        // This bails with InvalidOperationException with the message
        // "This method cannot be called during the application's pre-start 
        // initialization stage."
        return BuildManager.GetReferencedAssemblies();
    }

    //...
}

I found a SO commentary on this. I still wonder if there is something that can be manually initialized to make the above code happy. Anyone?

But in the absence of that...I can't help notice that the invocation comes from an implementation of IBuildManager. I explored the possibility of injecting my own IBuildManager, but I ran into the following problems:

  • IBuildManager is marked internal, so I need some other authorized derivation from it. It turns out that the assembly System.Web.Mvc.Test has a class called MockBuildManager, designed for test scenarios, which is perfect!!! This leads to the second problem.
  • The MVC distributable, near as I can tell, does not come with the System.Web.Mvc.Test assembly (DOH!).
  • Even if the MVC distributable did come with the System.Web.Mvc.Test assembly, having an instance of MockBuildManager is only half the solution. It is also necessary to feed that instance into the DefaultControllerFactory. Unfortunately the property setter to accomplish this is also marked internal (DOH!).

In short, unless I find another way to "initialize" the MVC framework, my options now are to either:

  • COMPLETELY duplicate the source code for DefaultControllerFactory and its dependencies, so that I can bypass the original GetReferencedAssemblies() issue. (ugh!)
  • COMPLETELY replace the MVC distributable with my own build of MVC, based on the MVC source code - with just a couple internal modifiers removed. (double ugh!)

Incidentally, I know that the MvcContrib "TestHelper" has the appearance of accomplishing my goal, but I think it is merely using reflection to find the controller - rather than using the actual IControllerFactory to retrieve a controller type / instance.

A big reason why I want this test capability is that I have made a custom controller factory, based on DefaultControllerFactory, whose behavior I want to verify.

Community
  • 1
  • 1
Brent Arias
  • 29,277
  • 40
  • 133
  • 234

1 Answers1

1

I'm not quite sure what you're trying to accomplish here. If it's just testing your route setup; you're way better off just testing THAT instead of hacking your way into internals. 1st rule of TDD: only test the code you wrote (and in this case that's the routing setup, not the actual route resolving technique done by MVC).

There are tons of posts/blogs about testing a route setup (just google for 'mvc test route'). It all comes down to mocking a request in a httpcontext and calling GetRouteData.

If you really need some ninja skills to mock the buildmanager: there's a way around internal interfaces, which I use for (LinqPad) experimental tests. Most .net assemblies nowadays have the InternalsVisibleToAttribute set, most likely pointing to another signed test assembly. By scanning the target assembly for this attribute and creating an assembly on the fly that matches the name (and the public key token) you can easily access internals.

Mind you that I personally would not use this technique in production test code; but it's a nice way to isolate some complex ideas.

void Main()
{
    var bm = BuildManagerMockBase.CreateMock<MyBuildManager>();
    bm.FileExists("IsCool?").Dump();
}

public class MyBuildManager : BuildManagerMockBase
{
    public override bool FileExists(string virtualPath) { return true; }
}

public abstract class BuildManagerMockBase
{
    public static T CreateMock<T>() 
        where T : BuildManagerMockBase
    {
        // Locate the mvc assembly
        Assembly mvcAssembly = Assembly.GetAssembly(typeof(Controller));

        // Get the type of the buildmanager interface
        var buildManagerInterface = mvcAssembly.GetType("System.Web.Mvc.IBuildManager",true);

        // Locate the "internals visible to" attribute and create a public key token that matches the one specified.
        var internalsVisisbleTo = mvcAssembly.GetCustomAttributes(typeof (InternalsVisibleToAttribute), true).FirstOrDefault() as InternalsVisibleToAttribute;
        var publicKeyString = internalsVisisbleTo.AssemblyName.Split("=".ToCharArray())[1];
        var publicKey = ToBytes(publicKeyString);

        // Create a fake System.Web.Mvc.Test assembly with the public key token set
        AssemblyName assemblyName = new AssemblyName();
        assemblyName.Name = "System.Web.Mvc.Test";
        assemblyName.SetPublicKey(publicKey);

        // Get the domain of our current thread to host the new fake assembly
        var domain = Thread.GetDomain();
        var assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
        moduleBuilder = assemblyBuilder.DefineDynamicModule("System.Web.Mvc.Test", "System.Web.Mvc.Test.dll");
        AppDomain currentDom = domain;
        currentDom.TypeResolve += ResolveEvent;

        // Create a new type that inherits from the provided generic and implements the IBuildManager interface
        var typeBuilder = moduleBuilder.DefineType("Cheat", TypeAttributes.NotPublic | TypeAttributes.Class, typeof(T), new Type[] { buildManagerInterface });      
        Type cheatType = typeBuilder.CreateType();

        // Magic!
        var ret = Activator.CreateInstance(cheatType) as T;

        return ret;
    }

    private static byte[] ToBytes(string str)
    {
        List<Byte> bytes = new List<Byte>();

        while(str.Length > 0)
        {
            var bstr = str.Substring(0, 2);
            bytes.Add(Convert.ToByte(bstr, 16));
            str = str.Substring(2);
        }

        return bytes.ToArray();
    }

    private static ModuleBuilder moduleBuilder;

    private static Assembly ResolveEvent(Object sender, ResolveEventArgs args)
    {
        return moduleBuilder.Assembly;
    }

    public virtual bool FileExists(string virtualPath)      { throw new NotImplementedException(); }
    public virtual Type GetCompiledType(string virtualPath) { throw new NotImplementedException(); }
    public virtual ICollection GetReferencedAssemblies()    { throw new NotImplementedException(); }
    public virtual Stream ReadCachedFile(string fileName)   { throw new NotImplementedException(); }
    public virtual Stream CreateCachedFile(string fileName) { throw new NotImplementedException(); }
}
null
  • 7,906
  • 3
  • 36
  • 37
  • 1
    I'm running into the same issue, trying to test a custom controller factory that pulls the controller from an IOC container. So my use case is to verify that the controller is pulling the correct controller from the IOC container in the GetControllerInstance() method (which is not directly testable because it is protected, and apparently can only be accessed through CreateController()). But the code does not get that far because of the exception being thrown. – Thierry Jun 12 '14 at 19:25