3

The WPF application I am working on will have numerous add-ins to extend its functionality. Each add-in will consist of one or several assemblies (usually a "main" add-in assembly with separate assemblies for presentation-layer components and views) but will be treated by the application as a single Prism module. Prism will be used to discover and load the add-ins, with MEF used to ensure that the Prism module can access dependencies in the add-in's related assemblies. The Prism module's Initialize method will be responsible for, amongst other things, configuring the IoC container (in this case, Unity). In a deployed scenario, this will all be loaded and managed with a MefBootstrapper at startup.

My problem occurs when trying to unit test an add-in. To keep the add-in code semi-isolated from the main application, each add-in will also have its own unit test assemblies. One of these test assemblies will be responsible for checking registration of services with the IoC container. In the test scenario, I don't want to use a bootstrapper to load the Prism modules as they have dependencies that I don't want to introduce to my test assemblies. I have therefore written my test fixture base class such that it creates its own MefModuleManager to load the module to be tested.

ResolutionTestBase.cs

public abstract class ResolutionTestBase
{
  [ClassInitialize]
  public static void TestFixtureInitialise(TestContext context)
  {
    // Create the main resolution container.
    var container = new UnityContainer();

    // Install the service locator.
    var locator = new UnityServiceLocator(container);
    ServiceLocator.SetLocatorProvider(() => locator);
  }

  // Here go some helper methods for performing resolution tests.

  protected IUnityContainer Container
  {
    get { return ServiceLocator.Current.GetService(typeof(IUnityContainer)) as IUnityContainer; }
  }
}

AddInResolutionTestBase.cs

public abstract class AddInResolutionTestBase:ResolutionTestBase
{
  static AddInResolutionTestBase()
  {
    Logger = new EmptyLogger();
  }

  [TestInitialize]
  public virtual void TestInitialise()
  {
    // Create MEF catalog.
    var aggregateCatalog = new AggregateCatalog();
    foreach (var testAssembly in TestAssemblies)
    {
      aggregateCatalog.Catalogs.Add(new AssemblyCatalog(testAssembly));
    }

    // Load module manager.
    var container = new CompositionContainer(aggregateCatalog);
    var serviceLocator = new MefServiceLocatorAdapter(container);
    var parts = new DownloadedPartCatalogCollection();
    var moduleInitialiser = new MefModuleInitializer(serviceLocator, Logger, parts, aggregateCatalog);
    var moduleManager = new MefModuleManager(moduleInitialiser, ModuleCatalog, Logger);
    moduleManager.ModuleTypeLoaders = new[] { new MefFileModuleTypeLoader() };
    moduleManager.Run();
  }

  protected static ILoggerFacade Logger { get; private set; }
  protected abstract IModuleCatalog ModuleCatalog { get; }
  protected abstract IEnumerable<Assembly> TestAssemblies { get; }
}

For the add-ins, they have a stand-alone class in their main assembly to implement the requirements of a Prism module, and Unity extensions to configure the container.

Module.cs

[ModuleExport("AddInModule", typeof(Module), InitializationMode = InitializationMode.OnDemand)]
public class Module : IModule
{
  private readonly IEnumerable<IUnityContainerExtensionConfigurator> _extensions;

  [ImportingConstructor]
  public Module([ImportMany]IEnumerable<IUnityContainerExtensionConfigurator> extensions)
  {
    _extensions = extensions;
  }

  public void Initialize()
  {
    // Load the dependency injection container.
    var container = ServiceLocator.Current.GetService(typeof(IUnityContainer)) as IUnityContainer;
    if (container != null)
    {
      foreach (var extension in _extensions)
      {
        container.AddExtension((UnityContainerExtension) extension);
      }
    }
  }
}

ContainerInstallerExtension.cs (in the add-in's main assembly)

[Export(typeof(IUnityContainerExtensionConfigurator))]
public class ContainerInstallerExtension : UnityContainerExtension
{
  protected override void Initialize()
  {
    // perform container configuration here.
  }
}

PresentationInstallerExtension.cs (in the add-in's presentation assembly)

[Export(typeof(IUnityContainerExtensionConfigurator))]
public class PresentationInstallerExtension:UnityContainerExtension
{
  protected override void Initialize()
  {
    // perform container configuration here.
  }
}

AddInResolutionTest.cs (in the add-in's IoC test assembly)

[TestClass]
public class AddInResolutionTest : AddInResolutionTestBase
{
  private IEnumerable<Assembly> _testAssemblies;

  private IModuleCatalog DoGetModuleCatalog()
  {
    var moduleInfo = new ModuleInfo("AddInModule", typeof (Module).AssemblyQualifiedName)
    {
      InitializationMode = InitializationMode.WhenAvailable,
      Ref = typeof (Module).Assembly.CodeBase
    };
    return new ModuleCatalog(new[] {moduleInfo});
  }

  protected override IModuleCatalog ModuleCatalog
  {
    get { return DoGetModuleCatalog(); }
  }

  protected override IEnumerable<Assembly> TestAssemblies
  {
    get { return _testAssemblies ?? (_testAssemblies = new[] { typeof(ContainerInstallerExtension).Assembly, typeof(PresentationInstallerExtension).Assembly }); }
  }

  [TestMethod]
  public void ResolveSomeService()
  {
    // perform resolution test here.
  }
}

Of note with the resolution test fixture, the "test assemblies" are linked to the IoC test assembly with project references and referred to directly by type (rather than using a directory-scan catalog) so I could avoid having to use a post-build event to copy assemblies to a common folder for testing.

When I run the unit tests (as-is), I get an exception indicating the module manager failed to load the Prism module:

Initialization method AddInResolutionTest.TestInitialise threw exception. Microsoft.Practices.Prism.Modularity.ModuleTypeLoadingException: Microsoft.Practices.Prism.Modularity.ModuleTypeLoadingException: Failed to load type for module AddInModule.

If this error occurred when using MEF in a Silverlight application, please ensure that the CopyLocal property of the reference to the MefExtensions assembly is set to true in the main application/shell and false in all other assemblies.

Error was: Object reference not set to an instance of an object.. ---> System.NullReferenceException: Object reference not set to an instance of an object.. at Microsoft.Practices.Prism.MefExtensions.Modularity.MefFileModuleTypeLoader.LoadModuleType(ModuleInfo moduleInfo) --- End of inner exception stack trace --- at Microsoft.Practices.Prism.Modularity.ModuleManager.HandleModuleTypeLoadingError(ModuleInfo moduleInfo, Exception exception) at Microsoft.Practices.Prism.Modularity.ModuleManager.IModuleTypeLoader_LoadModuleCompleted(Object sender, LoadModuleCompletedEventArgs e) at Microsoft.Practices.Prism.MefExtensions.Modularity.MefFileModuleTypeLoader.RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e) at Microsoft.Practices.Prism.MefExtensions.Modularity.MefFileModuleTypeLoader.RaiseLoadModuleCompleted(ModuleInfo moduleInfo, Exception error) at Microsoft.Practices.Prism.MefExtensions.Modularity.MefFileModuleTypeLoader.LoadModuleType(ModuleInfo moduleInfo) at Microsoft.Practices.Prism.Modularity.ModuleManager.BeginRetrievingModule(ModuleInfo moduleInfo) at Microsoft.Practices.Prism.Modularity.ModuleManager.LoadModuleTypes(IEnumerable`1 moduleInfos) at Microsoft.Practices.Prism.Modularity.ModuleManager.LoadModulesWhenAvailable() at Microsoft.Practices.Prism.Modularity.ModuleManager.Run() at AddInResolutionTestBase.TestInitialise() in AddInResolutionTestBase.cs: line xx

At the point of calling moduleManager.Run() nothing in my code is null so it is not clear to me what the "real" problem is.

I have tried various changes to resolve the problem including:

  • calling moduleManager.LoadModule() instead of moduleManager.Run() in AddInResolutionTestBase.cs
  • manipulating the State of the ModuleInfo created in AddInResolutionTest to bypass the problem in the module manager

Any other changes I've made have resulted in different errors, but still indicate a problem with the module manager trying to load the Prism module.

Is there some additional step required to correctly configure the module manager to be able to load modules in this way, bearing in mind that some of the usual overhead (such as the logger) is not required for the unit tests?

Matthew
  • 155
  • 1
  • 11

1 Answers1

1

With the aid of a de-compiler, I was able to figure out the "missing pieces" and make some changes to ensure that all the required components are registered/installed for the module manager to be able to initialise the module(s) for testing. For anyone who is interested:

public abstract class AddInResolutionTestBase:ResolutionTestBase
{
  private CompositionContainer _container;
  private IModuleCatalog _moduleCatalog;
  private IEnumerable<object> _testEntities;
  private IEnumerable<ModuleInfo> _testModuleInformation;

  static AddInResolutionTestBase()
  {
    Logger = new EmptyLogger();
  }

  [TestInitialize]
  public virtual void TestInitialise()
  {
    // Create MEF catalog.
    AggregateCatalog = CreateAggregateCatalog();
    ConfigureAggregateCatalog();
    AggregateCatalog = DefaultPrismServiceRegistrar.RegisterRequiredPrismServicesIfMissing(AggregateCatalog);

    ConfigureContainer();

    // Initialise modules to be tested.
    CompositionContainer.GetExportedValue<IModuleManager>().Run();
  }

  #region Protected Methods
  protected virtual void ConfigureAggregateCatalog()
  {
    var testAssemblies = TestEntities.OfType<Assembly>();
    foreach (var testAssembly in testAssemblies)
    {
      AggregateCatalog.Catalogs.Add(new AssemblyCatalog(testAssembly));
    }

    if (TestEntities.Any(entity => entity is System.Type))
    {
      var catalog = new TypeCatalog(TestEntities.OfType<System.Type>());
      AggregateCatalog.Catalogs.Add(catalog);
    }
  }

  protected virtual void ConfigureContainer()
  {
    CompositionContainer.ComposeExportedValue<ILoggerFacade>(Logger);
    CompositionContainer.ComposeExportedValue<IModuleCatalog>(ModuleCatalog);
    CompositionContainer.ComposeExportedValue<IServiceLocator>(new MefServiceLocatorAdapter(CompositionContainer));
    CompositionContainer.ComposeExportedValue<AggregateCatalog>(AggregateCatalog);
  }

  protected virtual AggregateCatalog CreateAggregateCatalog()
  {
    return new AggregateCatalog();
  }

  protected virtual CompositionContainer CreateContainer()
  {
    return new CompositionContainer(AggregateCatalog);
  }

  protected virtual IModuleCatalog CreateModuleCatalog()
  {
    return new ModuleCatalog(TestModuleInformation);
  }

  protected abstract IEnumerable<object> GetTestEntities();
  protected abstract IEnumerable<ModuleInfo> GetTestModuleInformation(); 
  #endregion

  #region Protected Properties

  protected AggregateCatalog AggregateCatalog { get; set; }

  protected CompositionContainer CompositionContainer
  {
    get { return _container ?? (_container = CreateContainer()); }
  }

  protected static ILoggerFacade Logger { get; private set; }

  protected IModuleCatalog ModuleCatalog
  {
    get { return _moduleCatalog ?? (_moduleCatalog = CreateModuleCatalog()); }
  }

  protected IEnumerable<object> TestEntities
  {
    get { return _testEntities ?? (_testEntities = GetTestEntities()); }
  }

  protected IEnumerable<ModuleInfo> TestModuleInformation
  {
    get { return _testModuleInformation ?? (_testModuleInformation = GetTestModuleInformation()); }
  } 
  #endregion
}

This test base class now mimics to some extent what normally goes on in the boostrapper when the application normally starts. The (resolution) tests in each of the add-ins now only need to provide a list of the (exported) container extensions and module information for the Prism module that represents the add-in (in addition to the actual resolution tests!)

Matthew
  • 155
  • 1
  • 11