PowerShell Pester: Write Unit Tests for Your Scripts

Scripts that work today break after a module update or a refactor next month. Without automated tests, you discover these regressions in production. PowerShell Pester testing is the solution: a unit test framework built for PowerShell that runs before deployment, catches broken functions, and gives you confidence that your changes do not silently break existing behavior. This post covers Pester 5 syntax, assertions, mocking, and CI integration.
Quick Answer / TL;DR
Install Pester 5 with Install-Module Pester -Force -SkipPublisherCheck. Write tests in a file named FunctionName.Tests.ps1. Run with Invoke-Pester -Path .\Tests.
Install and Configure Pester 5
Windows ships with Pester version 3, which has significantly different syntax. Always install Pester 5 from the PowerShell Gallery for new projects. Use -Force -SkipPublisherCheck to bypass the publisher mismatch with the built-in version.
# Install Pester 5 (overrides built-in Pester 3)
Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser
# Verify version
Import-Module Pester -Force
(Get-Module Pester).Version
Major Minor Build Revision
----- ----- ----- --------
5 6 1 0
Describe and It Blocks
Tests are organized in Describe blocks (one per function or feature) containing It blocks (one per test case). Use BeforeAll for setup that runs once per Describe block and BeforeEach for setup before every It. The function under test is dot-sourced in BeforeAll so it is available throughout the block.
# File: Get-ServerStatus.Tests.ps1
BeforeAll {
. "$PSScriptRoot\Get-ServerStatus.ps1"
}
Describe 'Get-ServerStatus' {
It 'Returns an object with Name property' {
$result = Get-ServerStatus -Name 'web01'
$result.Name | Should -Be 'web01'
}
It 'Returns Status property' {
$result = Get-ServerStatus -Name 'web01'
$result.Status | Should -Not -BeNullOrEmpty
}
It 'Throws when Name is empty' {
{ Get-ServerStatus -Name '' } | Should -Throw
}
}
Should Assertions
Pester 5 uses a fluent Should assertion syntax. The most commonly used assertions are:
Should -Be 'value'— exact equalityShould -BeExactly 'Value'— case-sensitive equalityShould -BeTrue/Should -BeFalseShould -BeNullOrEmpty/Should -Not -BeNullOrEmptyShould -Throw— verifies a script block throws an exceptionShould -Contain 'item'— collection contains valueShould -Match 'pattern'— regex match
Describe 'Should assertions demo' {
It 'String equality' {
'PowerShell' | Should -Be 'PowerShell'
}
It 'Numeric comparison' {
42 | Should -BeGreaterThan 10
42 | Should -BeLessThan 100
}
It 'Collection membership' {
@('a', 'b', 'c') | Should -Contain 'b'
}
It 'Exception thrown' {
{ 1 / 0 } | Should -Throw
}
It 'Null check' {
$null | Should -BeNullOrEmpty
'' | Should -BeNullOrEmpty
}
}
Mock External Commands
Mock replaces a cmdlet or function with a controlled stub for the duration of the test. This prevents tests from making real network calls, writing files, or modifying system state. Mocks are scoped to their enclosing Describe or It block.
Describe 'Send-Alert function' {
BeforeAll {
. "$PSScriptRoot\Send-Alert.ps1"
}
It 'Calls Invoke-RestMethod when alert is critical' {
Mock Invoke-RestMethod { return @{ status = 'ok' } }
Send-Alert -Message 'Disk full' -Severity 'Critical'
Should -Invoke Invoke-RestMethod -Times 1 -Exactly
}
It 'Does not call Invoke-RestMethod for info alerts' {
Mock Invoke-RestMethod {}
Send-Alert -Message 'Script started' -Severity 'Info'
Should -Invoke Invoke-RestMethod -Times 0
}
}
Test a Real Function
A complete test file for a function that converts file sizes. The test covers expected output, edge cases, and error conditions — the three categories every function test suite should address.
# File: ConvertTo-HumanSize.Tests.ps1
BeforeAll {
function ConvertTo-HumanSize {
param([long]$Bytes)
switch ($Bytes) {
{ $_ -ge 1GB } { return "$([math]::Round($_ / 1GB, 2)) GB" }
{ $_ -ge 1MB } { return "$([math]::Round($_ / 1MB, 2)) MB" }
{ $_ -ge 1KB } { return "$([math]::Round($_ / 1KB, 2)) KB" }
default { return "$_ B" }
}
}
}
Describe 'ConvertTo-HumanSize' {
It 'Returns bytes for small values' {
ConvertTo-HumanSize -Bytes 512 | Should -Be '512 B'
}
It 'Converts kilobytes correctly' {
ConvertTo-HumanSize -Bytes 2048 | Should -Be '2 KB'
}
It 'Converts megabytes correctly' {
ConvertTo-HumanSize -Bytes 5MB | Should -Match 'MB$'
}
It 'Converts gigabytes correctly' {
ConvertTo-HumanSize -Bytes 1GB | Should -Be '1 GB'
}
}
Run Tests in GitHub Actions CI
Add Pester to your CI pipeline to block merges when tests fail. The example below installs Pester, runs all test files, and exits with a non-zero code if any tests fail — causing the CI job to fail.
# .github/workflows/test.yml equivalent PowerShell step
# In your CI pipeline script or workflow:
Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser
$config = New-PesterConfiguration
$config.Run.Path = '.\Tests'
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'TestResults.xml'
$config.TestResult.OutputFormat = 'JUnitXml'
$result = Invoke-Pester -Configuration $config -PassThru
if ($result.FailedCount -gt 0) {
Write-Error "Pester: $($result.FailedCount) test(s) failed"
exit 1
}
Write-Host "All $($result.PassedCount) tests passed"
Common Errors and Fixes
- Pester 4 vs 5 syntax — major breaking changes between versions. Pester 5 changed assertion syntax (
Should -Beinstead ofShould Be), mock syntax (Should -Invokeinstead ofAssert-MockCalled), and configuration approach. If you see “Should: Parameter set cannot be resolved”, you are mixing syntax versions. Always specifyImport-Module Pester -MinimumVersion 5.0at the top of test files. - Mock scope limited to Describe block unless BeforeAll used. Mocks defined inside an
Itblock are scoped only to that test. Mocks inBeforeAllare available to allItblocks in the parentDescribe. Failing to understand scope is the most common cause of mock-related test failures.
Related Cmdlets / See Also
Wrapping Up
Pester transforms PowerShell scripting from a “deploy and hope” workflow to a tested, confident process. Write tests alongside every function, mock external dependencies to keep tests fast and isolated, and integrate with CI to block untested or broken code from reaching production. Start with three tests per function: happy path, edge case, and error condition.


