Unit Testing Custom Assembly Resolution

For my project at work I had to implement custom assembly resolution in a framework that supports dynamically loaded dlls stored at various locations, relative to the main executable. As an aside, this is achieved by listening to the AssemblyResolve event on the Current AppDomain (AppDomain.Current.AssemblyResolve). The handler should figure out the file name based on the assembly name and load the file using the Assembly.LoadFrom() method.


AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(HandleAssemblyResolve);

private static Assembly HandleAssemblyResolve(object sender, ResolveEventArgs args)
{
  //look at args.Name to figure out the filename your assembly would be defined in
  // ... skipping as its business specific
  //
  return LoadAssemblyFrom(foundPath, fileName);
}

private static Assembly LoadAssemblyFrom(string foundPath, string fileName)
{
   string text = Path.Combine(foundPath, fileName);
   Trace.TraceInformation("Found file {0}", text);
   return Assembly.LoadFrom(Path.Combine(foundPath, text));
}

But how does one go about unit testing this? The problem that .NET poses here is that once an assembly loaded into an appdomain, it cannot be unloaded from it. So the only way we can test this is by creating an appdomain to host loaded assemblies during testing and then unloaded it when the test is complete.

To accomplish this, I wrote this utility method called RunInAnotherAppDomain. Notice that it does not use the Action delegate, but instead uses a special CrossAppDomainDelegate class to enable calls across App Domains.


private void RunInAnotherAppDomain(CrossAppDomainDelegate actionToRun)
{
  var dom = AppDomain.CreateDomain("test", AppDomain.CurrentDomain.Evidence, 
                 AppDomain.CurrentDomain.BaseDirectory, string.Empty, false);

  dom.DoCallBack(actionToRun);
  AppDomain.Unload(dom);
}

In order to test this, I needed an assembly to play with. The easiest thing to do is to add a reference to another assembly from the project containing your unit tests. Because of the default Visual studio behavior, your assembly would be copied into the same directory as the unit test dll itself during compilation. I had created Library project, compiled it committed it to the library directory of my project and referenced it from the unit test project. The name of the dll was Dummy.dll and, and the name of the assembly was simply “Dummy”. Because it’s not actually used by the code directly (no code from dummy is actually being used, the assembly will not be loaded automatically by the process, which leaves open for me to play with.

The first test I would want to write would expect the assembly to be loaded normally, just by asking right out of the box.


[Test]
public void ShouldLoadAssembly()
{
  RunInAnotherAppDomain(() => 
  {
     //This method uses default .NET assembly resolution rules to load
     var assembly = Assembly.Load("Dummy");  
     Assert.IsNotNull(assembly);
  }
}

In the next test, I would move the assembly DLL to another location and will attempt to run same test and would expect it to fail:

[Test, ExpectException(ExpectedException = typeof(FileNotFoundException))]
public void ShouldFailLoadAssembly()
{
  MoveDummyAssemblyAway(); 
  try
  {
    ShouldLoadAssembly();
  }
  finally
  { 
    ReturnDummyAssemblyBack();
  } 
}
 
private static void MoveDummyAssemblyAway()
{ 
   if(!Directory.Exists("test"))
   { 
       Directory.CreateDirectory("test");
   }

   File.Copy("Dummy.dll" "test\\Dummy.dll", true);
   File.Delete("Dummy.dll");
}

private static void ReturnDummyAssemblyBack()
{
  if(Directory.Exists("test"))
  {  
      File.Copy("test\\Dummy.dll", "Dummy.dll", true);
      File.Delete("test\\Dummy.dll");
      Directory.Delete("test");
  }
}

I am now ready to test my custom assembly resolution logic.


[Test]
public void ShouldLoadAssemblyWithCustomResolution()
{  
  MoveDummyAssemblyAway(); 
  try
  {
    RunInAnotherAppDomain(() =>
    {
       //This sets up resolution for the current AppDomain.  
       //This lambda runs in the "test" appdomain, so it'll work
       MyCustomAssemblyResolver.Init();
       
       var assembly = Assembly.Load("Dummy");  
       Assert.IsNotNull(assembly); 
    }
  }
  finally
  { 
    ReturnDummyAssemblyBack();
  } 

If my custom resolution logic inside MyCustomAssemblyResolver class works, the test should pass.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s