Sunday, January 18, 2015

Creating your own unit testing framework for PowerShell - Part 3

The windows PowerShell engine can be hosted in the System.Management.Automation.PowerShell class. Using this class you can create, execute and manage commands in a Runspace. We’ll use these features of the PowerShell class to load and execute our Modules and interact with the PowerShell engine whenever needed in our unit test framework. While creating the PowerShell host for our test framework, it’s good to define the Runspace that is responsible for the operating environment for command pipelines. In our framework I preferred to use the constrained runspace which will allow us to restrict the programming elements that can be applied by the user.
We’ll later use this ability of constrained runspaces to simulate a Stub behavior in our test framework. To restrict the availability of aliases, applications, cmdlets, functions and scripts, we’ll create an empty InitialSessionState and use that for creating a runspace. Later we’ll use the AddPSModule, AddCommand, AddPSSnapin methods to include the required functionality in our runspace in a text context.

InitialSessionState has three different methods to create a container that holds commands
  • Create - Creates an empty container. No commands are added to this container.
  • CreateDefault - Creates a session state that includes all of the built-in Windows PowerShell commands on the machine. When using this API, all the built-in PowerShell commands are loaded as snapins.
  • CreateDefault2 - Creates a session state that includes only the minimal set of commands needed to host Windows PowerShell. When using this API, only one snapin – Microsoft.PowerShell.Core - is loaded.

We’ll use the CreateDefault2 overload method to create the InitialSesionState instance.

private InitialSessionState CreateSessionState(string path)
{
    var state = InitialSessionState.CreateDefault2();
    if (!String.IsNullOrEmpty(path))
    {
        state.ImportPSModulesFromPath(path);
    }
    return state;
}

In the CreateSessionState method, we use the Path property of the PSModuleAttribute created in the part 1 of this series to load all modules from the path provide to the InitilSessionState.

Once we have the InitialSessionState, we’ll use this container to create the Runspace and then the PowerShell host

_runspace = RunspaceFactory.CreateRunspace(state); 
_runspace.Open();
_shell = PowerShell.Create();
_shell.Runspace = _runspace;

We’ll use this PowerShell instance to execute the commands needed from the module. In the framework, I have my execute method created as

public TModule Execute(string method)
{
    if (_shell == null)
    {
        throw new ArgumentException("The PowerShell host should be setup before invoking the methods");
    }
    _shell.AddCommand(_moduleInfo.Name + @"\" + method);
    var methodProperties = typeof(TModule).GetProperties()
        .Where(prop => prop.IsDefined(typeof(PsModuleFunctionAttribute), false)).ToList();
    var property = methodProperties.First(p => p.GetCustomAttribute<PsModuleFunctionAttribute>().Name == method);
    var commandInfo = property.GetValue(_module) as PsCommandInfo;
    var parameters = commandInfo.Parameters;
    if (parameters != null)
    {
        _shell.AddParameters(parameters);
    }
    var results = _shell.Invoke();
    commandInfo.Result = results;
    property.SetValue(_module, commandInfo);
    DisposeContext();
    return _module;
}

As you can see from the highlighted code, we add the commands and the parameters to the shell using reflection from the metadata defined in the attributes to invoke the shell. The results are set back as property values to the ModuleObject and returned back to the test to assert the conditions.

A simple test code using the framework can be created like

var psHost = new PowerShellHost<XModule>();
var actual = psHost
    .Execute("Get-Greetings");

var result = actual.GetGreetings.Result.Select(psObject => psObject.BaseObject).OfType<string>().First();
Assert.AreEqual<string>(result, "Hello from VSTest");


Next in the series, we’ll see how to make use of our framework to stub methods/ cmdlets in the execution pipeline.

No comments: