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).  

3 Comments

  • This is a good approach, Daniel.

    Two other techniques that we use on the PowerShell team are:

    1) Hosting powershell (via our hosting / Runspace APIs.) In this, you create a pipeline, and execute your cmdlet that way. This lets you test cmdlets that derive from PsCmdlet, and still allows you to use the unit testing harness of choice.

    2) Writing scripts that test your cmdlet. This lets you test your cmdlet in the same environment that your users will run them from, and helps uncover some subtle usability issues and bugs. It requires that you write your own unit testing harness, though. I'll be blogging one soon, but it's not very difficult :)

    Lee

  • So how would you test pscmdlets rather than just plain vanilla cmdlets?

    Karl

  • Karl: looks like Lee suggestion (1) is the one to go for. I will try it later and let you guys know on my blog.

    Lee: I like 1). I'm not sure about 2), although I see the value in using the API from PS itself as a user would see it. I'm not sure how I would integrate that into my build-process, though...

    Thanks for the feedback!

Comments have been disabled for this content.