Testing with Pester
Prerequisites: Modules
Pester is PowerShell's testing framework. Tests describe expected behavior, and Pester checks it — so you can change scripts without fear.
Windows ships with the very old Pester 3.4; install the current version first:
# illustrative
Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser
A first test file
Test files end in .Tests.ps1:
Set-Content Get-Discount.ps1 @'
function Get-Discount {
param([double]$Total)
if ($Total -lt 0) { throw 'Total cannot be negative' }
if ($Total -ge 100) { return $Total * 0.1 }
0
}
'@
Set-Content Get-Discount.Tests.ps1 @'
BeforeAll {
. $PSScriptRoot/Get-Discount.ps1
}
Describe 'Get-Discount' {
It 'gives no discount under 100' {
Get-Discount -Total 50 | Should -Be 0
}
It 'gives 10% at 100 or more' {
Get-Discount -Total 200 | Should -Be 20
}
It 'rejects negative totals' {
{ Get-Discount -Total -5 } | Should -Throw 'Total cannot be negative'
}
}
'@
$result = Invoke-Pester -Path ./Get-Discount.Tests.ps1 -PassThru -Output None
"Passed: $($result.PassedCount), Failed: $($result.FailedCount)"
Passed: 3, Failed: 0
BeforeAllruns once before the tests — here it dot-sources the script under test.Describegroups tests; eachItis one test.Shouldmakes assertions:-Be,-BeExactly,-BeGreaterThan,-Contain,-Match,-BeNullOrEmpty,-Throw, and more.- Run
Invoke-Pesterin a folder to run every*.Tests.ps1file; the normal output lists each test with a green or red mark.
Test cases
Set-Content Math.Tests.ps1 @'
Describe 'Addition' {
It 'adds <A> and <B> to get <Expected>' -ForEach @(
@{ A = 1; B = 2; Expected = 3 }
@{ A = -1; B = 1; Expected = 0 }
@{ A = 2; B = 2; Expected = 5 }
) {
$A + $B | Should -Be $Expected
}
}
'@
$result = Invoke-Pester -Path ./Math.Tests.ps1 -PassThru -Output None
"Passed: $($result.PassedCount), Failed: $($result.FailedCount)"
$result.Failed | ForEach-Object { "$($_.ExpandedName): $($_.ErrorRecord[0].Exception.Message)" }
Passed: 2, Failed: 1
adds 2 and 2 to get 5: Expected 5, but got 4.
-ForEach runs one test per hash table; <A> placeholders in the name
are filled in. The last case was deliberately wrong, to show a failure.
Mocking
Mock replaces a command during a test, so you can test logic that
depends on things like the file system, the clock, or an API:
# illustrative
It 'reports when the service is down' {
Mock Invoke-RestMethod { throw 'timeout' }
Test-ServiceHealth | Should -Be 'DOWN'
Should -Invoke Invoke-RestMethod -Times 1
}