I write a lot of Powershell these days; it’s my go-to language for quick jobs that need to interact with external systems, like an API, a DB, or the file system for example.
Nothing to configure, nothing to deploy, nothing to set up really; just hack a script together and run it. Perfect for one-off little tasks.
I’ve used it for all manner of things in my career so far, most notably for Azure automation back before Azure had decent automation in place. We’re talking pre-Resource Manager environment creation stuff.
I would tie together a suite of separate scripts which would individually:
- create a Storage account,
- get the key for the Storage account,
- create a DB instance,
- execute a DB initialisation script,
- create a Service Bus,
- execute a Service Bus initialisation script,
- deploy Cloud Services,
- start/stop/restart those services
Tie that lot together and I could spin up an entire environment easily.
Yeah, but I’ve also used Powershell for completely random things, like:
- taking a list of email addresses which has been used to sign up on a web site,
- grab the MX record from each email address’s domain,
- TELNET into that MX endpoint,
- attempt to create a new email with the recipient set to that email address,
- listen out for a specific error code (550) which is returned when the user does not exist
All to validate a sign up list for marketing purposes! Once small script achieves this madness.
Powershell is the glue that holds a windows dev team together (unless there’s some actual Ops knowledge in that team..), allowing automation of non-functional requirements and repetitive tasks.
So what’s my point?
Who writes oodles of Powershell scripts, just like I do? Loads of you, I’ll bet.
Who writes tests for their Powershell scripts? I don’t. I doubt you do either.
Yet we rely on these scripts every bit as much as application code we push into a production environment; just because we don’t necessarily run them as often doesn’t make them any less fragile.
Enter Pester
“Pester provides a framework for running unit tests to execute and validate Powershell commands from within Powershell”
After installing a pester
module, you can invoke a command to create boilerplate function
and test
files – New-Fixture
.
Subsequently running Invoke-Pester
will execute the Powershell file ending with the name .Tests.ps1
against the other file that just ends in .ps1
.
Pester allows mocking of various core commands, for example Get-ChildItem
can be configured to always return a known set of items in order to validate functionality which may fork based on the contents of a directory.
Not just that, calls to these mocks can be asserted. Sounds to me like we have a basis for making our Powershell scripts testable and ultimately more reliable!
Working example
Let’s look at a simplified version of a script I was recently asked to write to automate a very labour-intensive task someone had been asked to do;
- for a directory of images
- if the image ends with “_A01.JPG”, create a duplicate and end that duplicate’s name with “_A99.JPG” instead of “_A01.JPG”
- if the image ends with “_ABC.JPG”, ignore it
- else create a duplicate and append “_A99.JPG” to the name
Looks like a great candidate for a Powershell script, right? You would probably jump right in and create a new directory, paste in an image and copy that image a few times, changing the name to match the use cases you’re working on.
Then you’d run the script against those test images, see if it worked, then delete the new images each time you wanted to run it again.
How about we do this properly instead?
1) Install Pester
Super simple:
Install-Module Pester
EASY, right?!
You do need
PSGet
installed in order to useInstall-Module
;PSGet
is likeChocolatey
for Powershell modules. Don’t havePSGet
? Go and install it!
2) Create a New-Fixture
Strangely enough, this is done by creating a new directory for your project and running:
New-Fixture -Name ImageRenamer
Which will result in two new files being created:
ImageRenamer.ps1
ImageRenamer.Tests.ps1
The files contain the following boilerplate code, respectively:
function ImageRenamer {
}
and
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "ImageRenamer" {
It "does something useful" {
$true | Should Be $false
}
}
3) Run the tests
To execute your test script, you simply use:
Invoke-Pester
which, given that rubbish boilerplate test script, results in:
Describing ImageRenamer
[-] does something useful 124ms
Expected: {False}
But was: {True}
at line: 7 in C:\pester\ImageRenamer.Test
s.ps1
7: $true | Should Be $false
Tests completed in 124ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0
4) Implement your own code
First up I’ll call the ImageRenamer
function within the test:
# ImageRenamer.Tests.ps1
Describe "ImageRenamer" {
It "should create a new image" {
ImageRenamer
}
}
Now I want to be able to check the image is created, right? Let’s implement some code to do that:
# ImageRenamer.ps1
function ImageRenamer {
$images = Get-ChildItem -Filter "*.JPG" .\
foreach($image in $images)
{
Copy-Item `
$image `
$image.Name.replace(".JPG", "_A99.JPG")
}
}
But since we don’t have any images, and we aren’t going to make any just to test our script, let’s get MOCKING!
5) Mocking
Our script currently depends on two core Powershell functions: Get-ChildItem
and Copy-Item
. Let’s mock em.
# ImageRenamer.Tests.ps1
Describe "ImageRenamer" {
It "should create a new image" {
Mock Get-ChildItem {
[PSCustomObject] `
@{ Name = 'shouldbecopied.JPG'; }
}
Mock Copy-Item {}
ImageRenamer
Assert-MockCalled Copy-Item -Times 1
}
}
So what’s happening here?
Mock Get-ChildItem {
[PSCustomObject] `
@{ Name = 'shouldbecopied.JPG'; }
}
When Get-ChildItem
is called, return a custom object that contains a property called Name
set to shouldbecopied.JPG
The other mock:
Mock Copy-Item {}
has an assertion
Assert-MockCalled Copy-Item -Times 1
The mock Copy-Item
doesn’t do anything, but it’s verifiable so I can check that it was called a specific number of times. Running Invoke-Pester
now results in:
Describing ImageRenamer
[+] should create a new image 51ms
Tests completed in 51ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0
Hooray! Let’s break it, just to check…
Mock Get-ChildItem {
@(
[PSCustomObject]`
@{ Name = 'shouldbecopied.JPG'; },`
[PSCustomObject]`
@{ Name = 'shouldbecopiedalso.JPG'; }
)
}
This should fail, right? We’re passing in two images, so Copy-Item
should get called twice instead of once.
Describing ImageRenamer
[+] should create a new image 111ms
Tests completed in 111ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0
…uh… wut?
Here’s a little secret: -Times
actually means “at least this many times”. We need to change it to:
Assert-MockCalled Copy-Item -Times 1 -Exactly
which results in
Describing ImageRenamer
[-] should create a new image 107ms
Expected Copy-Item to be called 1 times exactly but was called 2 times
at line: 516 in C:\Program Files\WindowsPowershell\Modules\Pester\3.3.5\Funct
ions\Mock.ps1
Tests completed in 107ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0
Thaaaat’s better!
Full scripts
Remember those example requirements?
- for a directory of images
- if the image ends with “_A01.JPG”, create a duplicate and end that duplicate’s name with “_A99.JPG” instead of “_A01.JPG”
- if the image ends with “_ABC.JPG”, ignore it
- else create a duplicate and append “_A99.JPG” to the name
First up: if the image ends with “_A01.JPG”, create a duplicate and end that duplicate’s name with “_A99.JPG” instead of “_A01.JPG”
Test
Context "if ends in '_A01', create duplicate replacing '_A01' with '_A99' " {
It "should create a duplicate with a new prefix" {
Mock Get-ChildItem {
@(
[PSCustomObject]@{ Name = 'img_A01.JPG'; }
)
}
Mock Copy-Item -ParameterFilter {
$Path.EndsWith("_A01.JPG") `
-and $Destination.EndsWith("_A99.JPG") `
-and -not $Destination.Contains("_A01")
}
ImageRenamer
Assert-MockCalled Copy-Item -Times 1 -Exactly
}
}
Context
is just a way of grouping multiple It
s together, potentially sharing common setup code and mocks.
Notice my Copy-Item
mock here uses a parameter filter which means the mock will only match if that parameter expression is true. I’m checking that the path
(i.e., source) ends with “_A01.JPG”, and the destination
ends with “_A99.JPG” and doesn’t contain “_A01” – this checks for the scenario where I rename from “img_A01.JPG” to “img_A01_A99.JPG” instead of “img_A99.JPG”.
Second one: if the image ends with “_ABC.JPG”, ignore it
Test
Context "if ends in '_ABC'" {
It "do nothing" {
Mock Get-ChildItem {
@(
[PSCustomObject]@{ Name = 'img_ABC.JPG'; }
)
}
Mock Copy-Item {}
ImageRenamer
Assert-MockCalled Copy-Item -Times 0
}
}
Notice that the Assert
here has -Times
set to 0
; when you use 0
then -Exactly
is implied.
Lastly: else create a duplicate and append “_A99.JPG” to the name
Test
Context "else " {
It "should create a new image with new suffix" {
Mock Get-ChildItem {
@(
[PSCustomObject]@{ Name = 'img.JPG'; }
)
}
Mock Copy-Item -ParameterFilter {
$Destination.EndsWith("_A99.JPG")
}
ImageRenamer
Assert-MockCalled Copy-Item -Times 1 -Exactly
}
}
Self explanatory, that one.
The resulting script
function ImageRenamer {
$images = Get-ChildItem -Filter "*.JPG" .\ `
| where {$_.Name -notlike "*ABC.JPG"}
foreach($image in $images)
{
$newextension = "_A99.JPG"
if ($image.Name -like "*_A01.JPG")
{
$newImageName = $image.Name.replace("_A01.JPG",$newextension)
}
else
{
$newImageName = $image.Name.replace(".JPG",$newextension)
}
Copy-Item -Path $image.Name -Destination $newImageName
}
}
Running this with Invoke-Pester
results in:
Describing ImageRenamer
Context if ends in '_A01', create duplicate replacing '_A01' with '_A99'
[+] should create a duplicate with a new prefix 131ms
Context if ends in '_ABC'
[+] do nothing 130ms
Context else
[+] should create a new image with new suffix 114ms
Tests completed in 376ms
Passed: 3 Failed: 0 Skipped: 0 Pending: 0
Summary
Hopefully that’s given you an easy introduction to unit testing your Powershell using pester.
For more info, head over to the repo on github. For the time being, I’m going to make a concerted effort to actually test my scripts from now on. You with me?
Excellent article … Many thanks for posting
Thanks so much for the wonderful and informative post. I never knew about this library, but it sure would have come in handy in the past if I had. I hope to explore it more and use it more in the future.
I just wanted to point out a few typos, that if someone were trying to follow along with the post, might get tripped up on. (Things don’t work if one were to copy and paste your above code into their own text editor and run them)
In Step 2, the New_fixture command should be updated from New-Fixture -Name MyImageRenamer -> New-Fixture -Name ImageRenamer
Also, in the MyImageRenamer.ps1 file, in the foreach loop, the third line should be changed from $image.replace(“.JPG”, “_A99.JPG”) -> $image.Name.replace(“.JPG”, “_A99.JPG”)
Thanks again for the post. I’m really excited to start using this new tool!
Good spot! Thanks!
Thank you for excellent information !!! I’ve got a starting point for testing powershell code
This is an awesome post! Thank you for the workflow. One thing I’m struggling with is when you need to test a script that starts with a Parameter block. In this case Pester asks immediately for the parameters when the standard New-Ficture is used:
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace ‘\.Tests\.’, ‘.’
. “$here\$sut”
How can one test for parameter input? And then later on for the functions within that script?