PowerShell: how to unit test your cmdlet
If you miss VS, intellisense, TD.NET, etc., you might want to try extending PowerShell with custom cmdlets, which are .NET classes deriving from Cmdlet. They allow you to extend PowerShell while still programming in your favorite language.
Read Pablo Galiano's post for a step-by-step introduction to Cmdlets.
I'm hooked to PowerShell. It's been really fun to learn, and I'm loving it.
I'm also hooked to Test Driven Design (that's what TDD should mean, IMO), so I naturally looked for a way to develop my cmdlets in a TDD way. Turns out that it's fairly easy. For this example, I will show a simple cmd-let that should load an assembly in a flexible way (by file name, full name or partial name).
First, create your unit test (ha! you thought I was going to create the cmdlet first?? :p):
[TestMethod]
public void ShouldCreateCmdLet()
{
LoadCommand cmd = new LoadCommand();
Assert.IsTrue(cmd is Cmdlet);
}
In order to get that test to compile, create your class deriving from CmdLet:
[Cmdlet("Load" , "Item", DefaultParameterSetName="Item")]
public class LoadCommand : Cmdlet
{
protected override void ProcessRecord()
{
base.ProcessRecord();
}
}
For comprehensive guidelines on CmdLet development, see the MSDN documentation.
Next, create the test that passes input to the cmdlet. Here's where the real cmdlet testing occurs:
[TestMethod]
public void ShouldLoadAssemblyWithFileName()
{
string asmFile = this.GetType().Module.FullyQualifiedName;
LoadCommand cmd = new LoadCommand();
cmd.Item = asmFile;
IEnumerator result = cmd.Invoke().GetEnumerator();
Assert.IsTrue(result.MoveNext());
Assert.IsTrue(result.Current is Assembly);
Assert.AreEqual(this.GetType().Assembly.FullName, ((Assembly)result.Current).FullName);
}
Note that there's no way to call ProcessRecord directly. The way to run the cmdlet is to call Invoke, and getting an enumerator from it. Remember that when placed in the pipeline, the cmdlet will be called once for each input in the pipeline. Let's now implement the cmdlet to make the test pass (and compile!):
[Cmdlet("Load" , "Item", DefaultParameterSetName="Item")]
public class LoadCommand : Cmdlet
{
private string assembly;
[Parameter(Mandatory=true, ValueFromPipeline=true, ParameterSetName="Item", Position=0, HelpMessageResourceId="LoadCmdlet_Item")]
public string Assembly
{
get { return assembly; }
set { assembly = value; }
}
protected override void ProcessRecord()
{
base.ProcessRecord();
if (File.Exists(assembly))
{
WriteObject(Assembly.LoadFrom(fileName));
return;
}
}
}
Note that in order to return output from your cmdlet, you call WriteObject. It's interesting to debug the test we wrote. You will notice that the call to Invoke doesn't actually execute the cmdlet. Instead, moving the enumerator does. This is how the pipeline achieves lazy evaluation of each "step" and continues executing with the following commands. Very cool.
And that's pretty much all there is to it. Now you can start adding tests and the corresponding features to your cmdlet, with the amazing piece of mind that comes from having a unit test that says that it actually works ;). Needless to say, this test-code-run is much faster than testing the cmdlet directly in PowerShell (you need to constantly exit PS and re-enter, re-add your snapin, etc., otherwise the output assembly gets locked).