Recently I was rewriting an old console tool written in c#. The tricky part was that I had to implement this tool as a PowerShell cmdlet written in C#. This was kind of fun and I like tasks like this, except for one thing. The tool connects to other systems and uses the file system which makes testing it a bit troublesome. In this post, I’m describing techniques that I tried and which worked (or not) for my purposes.
PowerShell in C#
There are tutorials showing how to write cmdlets in the c#. But, in short, I will tell you something about it. In my case, I was writing Invoke-CVApi cmdlet. C# class is named InvokeCVApiCommand. It derives from the base class responsible for handling connections to the main system (passwords, proxies etc.).
The base class, in turn, can be derived from Cmdlet or PSCmdlet (latter extends the first and adds some useful features I will present later). Both exist in System.Management.Automation namespace in PowerShellStandard.Library NuGet package and each cmdlet written in c# must derive from any.
My cmdlet-representing class is decorated with Cmdlet attribute with verb and noun provided. It also must contain a default, parameterless constructor.
I think that’s everything we have to know (except broad knowledge about PowerShell itself of course) in order to start coding cmdlets in C#. Now I’m going to show you the simplified cmdlet code – in the Pastebin service, (because I found WordPress Syntax Highlighter plugin worthless for examples more complicated than plain text).
The cmdlet is ready to be unit tested, so contains all necessary infrastructure that will be described later.
I removed most of the functionality from the cmdlet to make it clearer. It has two properties indicating paths to the output directory and to the file containing functions to be invoked.
It also has two constructors. Parameterless is needed in order to have cmdlet working. The second is needed to inject interfaces implementations.
Note ProcessInternal method. It’s internal, so invisible outside the package (not always true, as we will see soon). It will be used to unit test the cmdlet.
Also, the first line of the ProcessRecord method contains strange line:
Environment.CurrentDirectory = _contextProvider.GetCurrentDirectory(this);
It’s used to set the current directory when the cmdlet is invoked from the PS command line. Without this, the current directory can be a folder inside the Windows folder. So I want to set it to the directory from which the cmdlet is invoked. I’m using SessionState for this (SessionState.Path.CurrentSystemLocation property), and that’s why I derived my cmdlet from PSCmdlet, not the Cmdlet class.
How to test it? Most obvious way – manually
Most probably this is not what you want to read here 🙂 You want instructions on how to test automatically. But from my experience, I can only say that I always test my cmdlets manually. When all automatic tests are green, I always play with the PS console and try to see how the messages are displayed, whether something can be made better from the user’s perspective. I recommend doing this even if the cmdlet is covered by many automated tests. In order to do this, just open the console, import the library, see help for the cmdlet and try to run it.
Import-Module .\mymodule.dll -verbose help Invoke-CvApi Invoke-CvApi (...)
Invoking cmdlet from C# code
Instead of manually importing a module containing PS cmdlets, write command parameters and so on we can automate this activity and invoke the cmdlet from c# code – for example from the test library we can prepare. It’s relatively easy and the code for this is stated below:
Test library must have PowershellStandard.Libary package (our C# PS library containing cmdlets probably also, but it’s not required). Then we can create a test that will utilize PSHostHelper custom class that creates PS session and imports module from the specified directory (nasty directive #if there, not needed in most cases).
PowerShell object can be then used to invoke the command (method AddCommand) with parameters. As you can see, parameters can be as complex as needed – objects, collections, etc.
Invoke method returns collection of PSObjects but we know what the tested cmdlet returns (using WriteObject method) so we cast result to the appropriate class and do necessary assertions.
Unit testing of Cmdlet class
Custom cmdlets derived from Cmdlet class are relatively easy to be tested. This simple code is enough to do this. Remember, that if you try this for cmdlet derived from PSCmdlet, the exception „Cmdlets derived from PSCmdlet cannot be invoked directly.” will be thrown.
Unit testing of PSCmdlet class
Last, but most widely used (by me) way of testing. Almost all my cmdlets derive from PSCmdlet, so I cannot use the previous one. This time we cannot use Invoke function directly, so a bit more preparation is needed.
The first thing we have to do is to add a method that can be invoked in our test project. Remember this ProcessInternal method inside my cmdlet? This one will be used. As you noticed, it’s internal, but no problem – all we have to do is to decorate the library containing cmdlets with the InternalsVisibleTo attribute.
We can now set up our cmdlet in the test project (as we did in the previous section). Then invoke ProcessInternal. But it won’t work as an exception will be thrown. We have to set cmdlet’s CommandRuntime property with some kind of proxy object that will emulate the PS environment.
This works like a charm. Remember that PowershellEmulator can be mocked using the Moq library, as it implements the ICommandRuntime interface. For my purposes, it’s easier to have it as a class.
And this also concludes this post. I described different types of testing – from the simplest to the most complex. They all work for me and I hope you will also find them useful.
I haven’t described implementations of all interfaces that I inject inside the cmdlet. I think I’ll do this in the next post. If you’re curious how I mock dates, you can check it here (Polish language). I also could mention Pester, but I wanted to focus on tests written in C# code.