If you want to ensure your PowerShell code is in tip-top shape, you need to be unit testing it. Pester is a popular unit-testing framework built for PowerShell code that allows you to ensure the code you write is as you expect and stays that way.
When unit testing, it’s essential to ensure your code isn’t influenced in any way by the environment its being run on or by any other outside functions or modules. Unit testing tests individual units of code as one to ensure developers can accurately determine if a single “unit” behaves as expected.
One way to ensure unit tests are accurate and unaffected by other code is through a concept called mocking. Mocking is a feature in Pester that allows you to “replace” commands your “unit” is calling with ones of your own. Mocking enables you to set up various scenarios commands inside of your testing “unit” will adhere to figure out what might happen given various circumstances.
If this doesn’t make sense now, hopefully, a brief demonstration will make the light bulb turn on! To follow along with the demo, I will expect you to have a beginner-intermediate level understanding of the Pester testing framework.
If you’d like to dive deep into mocking and many real-world examples, be sure to check out The Pester Book.
Mocking Demonstration
Let’s say you have a PowerShell function that creates a file only if that file doesn’t already exist. To do this, you first check to see if that file exists with the Test-Path
command and if it returns False
, you then create the file.
Your function looks like this:
function New-FictionalFile {
[CmdletBinding()]
param(
[Parameter()]
[string]$FilePath
)
if (-not (Test-Path -Path $FilePath)) {
$null = New-Item -Path $FilePath -ItemType File
}
}
A typical Pester test for this function would look like below. I’m assuming the function above is in a script called C:\New-FictionalFile.ps1.
The test below is testing to ensure the function creates the file if it doesn’t already exist. This is great, but it has two problems; it depends on the environment (the storage to hold the file), and it’s not testing the scenario when the file already exists.
describe 'New-FictionalFile' {
context 'when the file path does not exist' {
## Ensure the test file isn't there
$null = Remove-Item -Path '~\file.txt' -ErrorAction Ignore
$null = New-FictionalFile -FilePath '~\file.txt'
it 'creates the file' {
'~\file.txt' | Should -Exist
}
}
}
To build a proper unit test, you must remove the environment requirement from the test. You must also test the scenario when the file already exists. To do that, you need to ensure the New-Item
cmdlet is not called. The only way to solve these two problems is to use mocking.
You need to mock or “replace” the functionality of both the Test-Path
and New-Item
cmdlets to control their output without relying on the environment.
You can see below that the code to ensure a file is created or removed (New-Item
and Remove-Item
) as well as mocking code added. Since you’ve now taken control of the output of all commands, there’s no need to depend on the environment anymore. Now, this unit test can be run anywhere without regard for the local filesystem.
describe 'New-FictionalFile' {
## This ensures New-Item will never run. It's just being used as a
## flag to test if it attempts to execute
mock 'New-Item'
context 'when the file path does not exist' {
## This ensures Test-Path always returns $false "mimicking" the file does not exist
mock 'Test-Path' { $false }
$null = New-FictionalFile -FilePath '~\file.txt'
it 'creates the file' {
## This checks to see if New-Item attempted to run. If so, we know the script did what we expected
$assMParams = @{
CommandName = 'New-Item'
Times = 1
Exactly = $true
}
Assert-MockCalled @assMParams
}
}
context 'when the file path already exists' {
## This ensures Test-Path always returns $true "mimicking" the file does not exist
mock 'Test-Path' { $true }
$null = New-FictionalFile -FilePath '~\file.txt'
it 'does not attempt to create a file' {
## This checks to see if New-Item did not attempt to run (Times = 0). If it did not
## that means that it did not attempt to create the file
$assMParams = @{
CommandName = 'New-Item'
Times = 0
Exactly = $true
}
Assert-MockCalled @assMParams
}
}
}
To learn more about the
Assert-MockCalled
command, be sure to check out the Pester documentation.
Summary
The concept of mocking in Pester can be confusing for those new to unit testing. It’s a powerful feature to control your code better and to ensure tests run the same regardless of the environment they’re in.
If you’d like to learn more about mocking in Pester, check out The Pester Book. In the book, you’ll learn mocking works at a deep level and how to apply it across many different scenarios.