So you’re venturing into PowerShell cmdlet development in C# are you? Good for you! But how will you unit test those little buggers?
Some people write their unit tests in PowerShell using Pester which seems less ideal to me. At that point, you are left in an awkward position when it comes to mocking because all your mockable interfaces will be in C#, so it’ll be too late to replace them. Also it takes you away from the ecosystem you are used to; XUnit, NSubstitute (or Moq) and your beloved C# of course.
The Common Solution
Many people take Daniel Schroeder’s documented approach and write a unit test that looks something like this:
|
|
This is very neat and seems to work well on the surface, but it’s actually not a great solution when you start dealing with non-terminating errors and trying to measure your code coverage.
Let’s get stuck into the details!
How Cmdlets Actually Work
Let’s start by looking at how Invoke
actually works in the
abstract Cmdlet class:
|
|
Note: I have removed some unnecessary lines and re-arranged the methods for clarity.
So let’s break this down:
Invoke
simply callsGetResults
and returns an iterator of all the items returned by itGetResults
creates a default runtime if one isn’t provided and passes an empty list of objects into its constructor (the purpose of this is so that the command runtime can fill that list up with all objects returned by the cmdlet)GetResults
then calls the three processing methods for executing each stage of the cmdlet
Our cmdlet will primarily call three methods to display output; WriteObject
, WriteWarning
and
WriteError
so let’s check out what they do (within the same Cmdlet abstract class);
|
|
Note: Once again, I’ve removed null checks which won’t even fail in this case.
So these methods simply call the related method in the command runtime. This command runtime sure is looking important hey?
OK! So let’s now look at the DefaultCommandRuntime
and see what these respective methods do:
|
|
So really there’s nothing that complex going on here:
DefaultCommandRuntime
will fill theresult
list declared inGetResults
with any objects that are written viaWriteObject
- It will do nothing when
WriteWarning
is called - It will throw an exception when
WriteError
is called
Now why is this all a problem?
-
We will always need to iterate over the results to run
Invoke
even if we are not expecting any at all. This can be done in many ways, but all of those ways are a bit ugly:1 2 3 4 5
// Capture the results using a generic object type and store them in a throwaway variable var _ = cmdlet.Invoke().OfType<object>().ToList(); // Loop over all results using a throwaway variable for each item foreach (var _ in cmdlet.Invoke()) {}
-
We won’t be able to validate any of warnings as they are not captured at all.
-
And most importantly, if a non-terminating error is written using
WriteError
in your cmdlet, an exception will be raised and all other results (created withWriteObject
) will be lost.Further to this, Coverlet and OpenCover won’t detect the closing braces in your call to
WriteError
as covered because the exception is thrown inWriteError
itself, not your code.See this issue for more information.
The Solution
Before I share the solution, I have to give full credit to Andrew Theken for his MockCommandRuntime implementation that heavily inspired my simplified implementation.
First, we can completely avoid the use of the iterator in our unit tests by simply creating our own base class with a method that simply calls the processing methods:
|
|
From this point forward, we will call Execute
in our tests instead of Invoke
. But now, we need
to create our own command runtime that will capture output, errors and warnings without throwing
exceptions.
Due to the fact that output can be any object, we’ll make this a generic class so that the calling test can specify the type it wants to receive for output.
Sadly we can’t inherit from DefaultCommandRuntime
as it is internal
, however we’ll
use it
as our starting point.
I will only show the attributes and methods that differ from this implementation below for clarity.
|
|
Simple right? So now we simply capture our errors and warnings and help ourselves out by casting
the output to the template type requested when the MockCommandRuntime
is created in tests.
Furthermore, the attributes that capture all forms of output are marked as public
so they are
accessible to the test.
Writing a Unit Test Using MockCommandRuntime
Now, as long as our cmdlet inherits from OurCmdlet
, our unit test will look something like this:
|
|
And what if the cmdlet throws a non-terminating error?
|
|
Coverage reporting will work perfectly too and you will also be able to check any combination of
Output
, Warnings
and Erorrs
if your cmdlet intends to return some objects, write some
warnings and write some non-terminating errors too.
Go Forth and Test
So there you go! You may now put aside your Pester library and enjoy writing tests with your favourite C# test and mocking framework!