Ugly Stool Rotating Header Image

November, 2010:

Unit Testing PowerShell Snapin

I am writing a PSDrive for PowerShell, and finally figured out how to unit test it without having to explicitly installing the assembly.

The expected flow for PowerShell plugins is the following.

  1. Write a PowerShell plugin.
  2. Register the plugin using installutil.exe.
  3. Add the PSSnapIn from your PowerShell session using Add-PSSnapIn.
  4. Use your PowerShell plugin for glory and profit.

As part of my unit tests I do not want to exec installutil, nor do I want to pollute the existing PowerShell snapins with the one under test, i.e. have the snapin installed twice.  If I am running a unit test for a snapin that is already installed I do not want to use the installed version.  I want the test version to temporarily remove the installed version, and use the snapin under test for the unit tests.  Unfortunately, there does not appear to be any public API to do this, so I used Reflector to figure out how to do it via a backdoor mechanism.

A PowerShell snapin is represented in code as a PSSnapInInfo class.  The list of snapins can be viewed with the Get-PSSnapin cmdlet.  The information contained in a PSSnapInInfo instance can be viewed with the following PowerShell snippet.

$snapin = (Get-PSSnapin)[0]
$snapin | Format-List *

When a snapin is registered with installutil a registry key is created under the key HKLM\Software\Microsoft\PowerShell\1\PowerShellSnapIns.  These registry keys are read by PowerShell at load time to populate the list of available PowerShell snapins.

I do not want unit tests to modify the registry, but I do want to get a hold of the list populated by the registry keys.  A little Reflector magic is necessary, but it eventually leads to four important classes: PSSnapInInfo, MshConsoleInfo, RunspaceConfigurationForSingleShell, RunspaceConfiguration.

PSSnapInInfo holds information about PowerShell snapins.  An instance of this class must be created about the snapin under test.

MshConsoleInfo contains the list of snapins available to a PowerShell session.  The list of snapins can be accessed by the SnapIns property.

RunspaceConfiguration is an abstract base class.  A concrete implementation of this class is RunspaceConfigurationForSingleShell, which contains an instance of MshConsoleInfo.  MshConsoleInfo can be accessed by the ConsoleInfo property.

The solution to avoiding installutil.exe, and programmatically inserting a snapin for the purpose of unit testing is to create a PSSnapInInfo instance, and add it to MshConsoleInfo.  None of these classes are publicly available, so reflection is used to access them.

Step 1, create an instance of PSSnapInInfo.

private static PSSnapInInfo GetPSSnapInInfo(
    string snapInName,
    string absoluteAssemblyPath,
    List types,
    List formats)
{
    Assembly assembly = Assembly.LoadFile(absoluteAssemblyPath, null);
    ConstructorInfo constructorInfo = typeof(PSSnapInInfo).GetConstructor(
        BindingFlags.NonPublic | BindingFlags.Instance,
        null,
        new Type[] {
            typeof(string)/*name*/,
            typeof(bool)/*isDefault*/,
            typeof(string)/*applicationBase*/,
            typeof(string)/*applicationName*/,
            typeof(string)/*moduleName*/,
            typeof(Version)/*psVersion*/,
            typeof(Version)/*version*/,
            typeof(Collection)/*types*/,
            typeof(Collection)/*formats*/,
            typeof(string)/*descriptionFallback*/,
            typeof(string)/*vendorFallback*/,
            typeof(string)/*customPSSnapInType*/},
        null);
    Collection myTypes = new Collection(types);
    Collection myFormats = new Collection(formats);
    object[] parameters = new object[] {
        snapInName/*name*/,
        true/*isDefault*/,
        System.IO.Path.GetDirectoryName(absoluteAssemblyPath)/*applicationBase*/,
        assembly.FullName/*assemblyName*/,
        absoluteAssemblyPath/*moduleName*/,
        new Version(2, 0)/*psVersion*/,
        assembly.GetName().Version/*verison*/,
        myTypes/*types*/,
        myFormats/*formats*/,
        String.Format("{0} SnapIn for Unit Teting", snapInName)/*descriptionFallback*/,
        "nUnit"/*vendorFallback*/,
        null/*customPSSnapInType*/};
    PSSnapInInfo info = (PSSnapInInfo)constructorInfo.Invoke(
        BindingFlags.NonPublic | BindingFlags.Instance,
        null,
        parameters,
        CultureInfo.CurrentCulture);
    return info;
}

Step 2, get the MshConsoleInfo instance from RunspaceConfiguration.

private object GetMshConsoleInfo(
        RunspaceConfiguration configuration)
{
    object consoleInfo = configuration.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance).First(
        x => x.Name == "ConsoleInfo").GetValue(configuration, null);
    return consoleInfo;
}

Step 3, add PSSnapInInfo for my snapin under test to the list of available snapins.

private void AddPSSnapInToRunspaceConfiguration(
    PSSnapInInfo snapInInfo,
    RunspaceConfiguration configuration)
{
    object consoleInfo = this.GetMshConsoleInfo(configuration);
    var properties = consoleInfo.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
    Collection snapIns = (Collection)consoleInfo.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance).First(
        x => x.Name == "PSSnapIns").GetValue(consoleInfo, null);
    snapIns.Add(snapInInfo);
}

Step 4, load the PSSnapInInfo.

private void LoadPSSnapIn(
    PSSnapInInfo snapInInfo,
    RunspaceConfiguration configuration)
{
    MethodInfo methodInfo = configuration.GetType().GetMethod(
        "LoadPSSnapIn",
        BindingFlags.NonPublic | BindingFlags.Instance,
        null,
        new Type[] { typeof(PSSnapInInfo) },
        null);
    methodInfo.Invoke(configuration, new object[] { snapInInfo });
}

Viola!

Page optimized by WP Minify WordPress Plugin