Click here to Skip to main content
15,566,114 members
Please Sign up or sign in to vote.
5.00/5 (1 vote)
I am changing a project of mine to use plug-ins for certain components (called FeedReaders), instead of having them defined in the main solution. I am following the technique present here by Duke of Haren. I am also using NUnit as by unit testing framework.

So I have created a FeedReaderPool class which will find and load the plug-in DLLs. It does so using this code:
private IEnumerable<INewFeedReader> LoadPluginDlls()
{
  // Load the DLLs from the plugins directory
  _fs.Directory.GetFiles(PluginDirectory)
               .Where(fname => fname.EndsWith(".dll"))
               .ToList()
               .ForEach(dll => Assembly.LoadFile(_fs.Path.GetFullPath(dll)));

  // Get the types for the DLLs that implement INewFeedReader
  var pluginMasterType = typeof(INewFeedReader);
  var pluginTypes = 
    AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(ass => ass.GetTypes())
    .Where(t => t.IsClass && pluginMasterType.IsAssignableFrom(t))
    .ToList();

  // Create instantiations of the plugin types
  var plugins = 
    pluginTypes
    .Select(pit => (INewFeedReader)Activator.CreateInstance(pit));

  return plugins;
}
The ToList() has been added just for debugging.

Now, I have a unit test, ValidateFailsWhenNoPluginsLoaded, that ensures when the FeedReaderPool runs the above code that it will throw an exception if it returns no plugins. I am using the System.IO.Abstractions package to create a mock FileSystem so that PlugInDirectory will be a valid directory with no files in it.
[Test]
public void ValidatesFailsWhenNoPluginsLoaded()
{
  // Arrange
  var testName = "testPool";
  var testDir = @"C:\testdir";

  var testFileSystem = new MockFileSystem();
  testFileSystem.AddDirectory(testDir);

  var expectedMessage = $"{testName}: PluginDirectory does not contain any plugins [";

  var pool = new FeedReaderPool(testName, testFileSystem)
  {
    PluginDirectory = testDir,
  };

  // Act & Assert
  var ex = Assert.Throws<InvalidOperationException>(() => pool.ValidateIsReadyToWork(), "Improperly initialized pool did not fail validation");
  Assert.That(ex.Message, Does.StartWith(expectedMessage), "Incorrect exception message for missing plugins");
}
When I run this test on its own it works. But when I run it with other unit tests it fails, indicating that it did find DLL to load.

I inserted the ToList() in LoadPluginDlls and set a breakpoint. What I see is that an INewFeedReaderProxy type has been found in the DynamicProxyGenAssembly2 assembly. My other unit tests are creating mock INewFeedReader objects and this is causing my LoadPluginDlls to accept the Moq-generated Types as valid plugins.

I know I could change LoadPluginDlls to exclude Types created by Moq (
.Where(ass => !ass.AssemblyQualifiedName.StartsWith("Castle")
), or something similar. But that seems wrong to me. Why should my 'production' code have to know about unit testing artifacts? It seems to me that there should be a way, in my unit tests, to clear out any of these generated Types before calling my test.

Does anyone know how I would do that?

What I have tried:

Moq documentation does not contain an answer, or I just can't tell how to find it (probably the later).
Could not find an answer through Google.

Or is there a more generic way to ensure that LoadPluginDlls only loads the DLLs I want? Can I tell that the Types I find are not from a valid directory, or something like that?
Posted
Updated 7-Jun-22 6:30am
v2
Comments
BillWoodruff 9-Aug-21 1:35am    
That's an interesting question ! If you don't get a response here in a few days, perhaps repost in the C# language forum after removing this post.
Richard Deeming 9-Aug-21 3:47am    
I suspect the file system is irrelevant; it's simply that the current AppDomain contains a loaded assembly with classes which implement your interface, which has been created as part of a different unit test.

Maybe you could execute that specific unit test in a separate AppDomain, as described in this thread:
Application.Current is null when running unit tests in Visual Studio 2008[^]
C Pottinger 13-Aug-21 15:06pm    
Thanks Richard. But that does seem like an awful lot of work to go through just to not have Moq interfere with Assembly.GetTypes().

In the end, it turned out I had to refactor my code, for unrelated reasons, and in doing so it allowed me to use dependency injection to provide a class that does the actual loading of the Types from a dll. I can now mock this new IPluginTypesLoader interface and avoid calling System.Reflection.Assembly.GetTypes() from within my unit tests.
Gerry Schmitz 11-Aug-21 11:55am    
Conditional compilation (#DEFINE; etc) may satisfy your desire to separate production from unit testing code.
C Pottinger 13-Aug-21 15:14pm    
I see what you are saying, but
a) I put production in quotes because what I really wanted to refer to was my 'program code' as opposed to my 'testing code'. I think my program code should not have to know anything about my testing code.
b) #DEFINE would be fine to separate my debugging code from my final code. But even if I did that, I would still have the issue that my program code contains knowledge of my unit tests.
c) Even with conditional compilation, I would still run into the issue that Assembly.GetTypes() would pick up types dynamically generated by Moq whenever I unit test.

1 solution

I had same problem and tried some workarounds

1) make your plugin classes to be necessarily sealed and filter only by sealed types

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && p.IsSealed
					).ToList()



2) make sure you are instatiating classes with one public parameterless constructor

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && p.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).Any(x => x.IsPublic  && x.GetParameters().Length == 0)
					).ToList()


or

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && p.GetConstructor(Type.EmptyTypes) != null
					).ToList()


or

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && p.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null) != null
					).ToList()


if you want nonpublic constructors

3) create a custom attribute, make your plugin classes do necessarily be decorated with this attribute

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && p.GetCustomAttributes(true).Any(x => x.GetType().Name == "MyCustomAttribute")
					).ToList()


4) Filter type name to not equal YourInterfaceName+"Proxy"

C#
var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p =>
                        type.IsAssignableFrom(p)
                        && !p.IsInterface
                        && !p.IsAbstract
                        && p.IsClass
                        && !p.Name.Contains("Proxy")
					).ToList()
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900