Configures Pester v5 for testing PowerShell CLIs, scripts, and cmdlets - Describe/Context/It blocks, Should assertions, Mock for isolating external dependencies, BeforeAll/BeforeEach setup hooks, Invoke-Pester with PesterConfiguration for tags, code coverage, and NUnit/JUnit XML output in CI. Use when the unit-under-test is a PowerShell script, function, or CLI tool invoked from pwsh on Windows or cross-platform.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Per pester-install, Pester is the standard PowerShell test framework for
unit, integration, and acceptance testing of scripts, functions, modules, and
CLI tools invoked from pwsh. It supports Windows PowerShell 3.0 - 5.1 and
PowerShell 6.0.4+ across Windows, Linux, and macOS. When the unit-under-test
is a shell script or binary on Linux/macOS, use bats-testing instead; Pester
is the correct choice whenever the subject or test is PowerShell.
Per pester-install, Windows 10 / Server 2016+ ship with Pester 3.4.0
built-in. The bundled version cannot be updated via Update-Module alone
because its publisher certificate differs from the community-signed Gallery
version. Install Pester v5 side-by-side:
# Windows (5.1 or pwsh 7+): -Force enables side-by-side; -SkipPublisherCheck
# accepts the newer certificate
Install-Module -Name Pester -Force -SkipPublisherCheck
# Linux / macOS (pwsh 7+): no bundled version to conflict with
Install-Module -Name Pester
# Verify
Import-Module Pester -PassThruPer pester-quick-start:
Test files must follow the *.Tests.ps1 naming convention. The outer
BeforeAll block dot-sources the script under test so its functions are
available to every nested block:
# Get-Greeting.Tests.ps1
BeforeAll {
. $PSScriptRoot/Get-Greeting.ps1
}
Describe 'Get-Greeting' {
It 'returns a greeting for the given name' {
Get-Greeting -Name 'Alice' | Should -Be 'Hello, Alice!'
}
}Run with:
Invoke-Pester -Output Detailed .\Get-Greeting.Tests.ps1Per pqs, Context and Describe are nearly interchangeable. Use
Describe at the top level (function or CLI command name) and Context to
group scenarios:
Describe 'Invoke-Deploy' {
Context 'when the target environment is valid' {
It 'exits 0 and logs a success message' {
# ...
}
}
Context 'when credentials are missing' {
It 'throws a terminating error' {
# ...
}
}
}Per pqs, Should is the assertion command. Common matchers:
$result | Should -Be 'expected' # strict equality
$result | Should -BeExactly 'Expected' # case-sensitive equality
$result | Should -BeLike '*partial*' # wildcard match
$result | Should -Match 'regex\d+' # regex match
$result | Should -BeNullOrEmpty # null or empty string/array
$result | Should -BeGreaterThan 0
{ risky-call } | Should -Throw # expects a terminating error
{ risky-call } | Should -Throw '*message*' # error message wildcard
$result | Should -Not -Be $null # negated formPer pqs, lifecycle hooks load fixtures and reset state.
BeforeAll runs once per block; BeforeEach runs before every It:
Describe 'Get-Report' {
BeforeAll {
. $PSScriptRoot/Get-Report.ps1
$script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $script:TempDir | Out-Null
}
AfterAll {
Remove-Item -Recurse -Force $script:TempDir
}
BeforeEach {
# reset any per-test state here
}
It 'writes a report file to the output directory' {
Get-Report -OutputPath $script:TempDir
Test-Path (Join-Path $script:TempDir 'report.csv') | Should -BeTrue
}
}Use $script: scope when a variable set in BeforeAll must be read inside
It blocks. Variables declared with plain $var in BeforeAll are not
visible inside It due to PowerShell scoping rules.
Per pester-mock:
"Mock mocks the behavior of an existing command with an alternate implementation."
Describe 'Send-Notification' {
BeforeAll {
. $PSScriptRoot/Send-Notification.ps1
Mock Invoke-RestMethod {
return @{ status = 'ok' }
}
Mock Write-Error {} # suppress error output in test runs
}
It 'calls Invoke-RestMethod once with the correct URI' {
Send-Notification -Message 'deploy done'
Should -Invoke Invoke-RestMethod -Times 1 -Exactly
}
It 'passes a ParameterFilter to scope the mock to specific arguments' {
Mock Get-Date { return [datetime]'2024-01-01' } -ParameterFilter {
$Format -eq 'yyyy-MM-dd'
}
$result = Get-FormattedDate
$result | Should -Be '2024-01-01'
}
}Per pm, mocks placed in BeforeAll apply to all It blocks in the
enclosing Describe/Context; a mock inside an It block scopes only to
that test. Use -Scope to override. Should -Invoke verifies call count:
Should -Invoke -CommandName Invoke-RestMethod -Times 1 `
-ParameterFilter { $Uri -like '*api.example.com*' }Per pm, $PesterBoundParameters (available since Pester 5.2.0)
replaces $PSBoundParameters inside mock script blocks.
Per pester-tags:
Tags can be applied to Describe, Context, and It blocks. Run a subset
by filtering on tags:
Describe 'Invoke-Deploy' -Tag 'Integration' {
Context 'slow path' -Tag 'Slow' {
It 'completes a full deployment cycle' -Tag 'E2E' {
# ...
}
}
}# Run only Integration tests, excluding slow ones
Invoke-Pester $path -TagFilter 'Integration' -ExcludeTagFilter 'Slow', 'WindowsOnly'Per pt, tag matching uses -like comparison, so wildcards work:
Invoke-Pester $path -ExcludeTagFilter 'Slow*', '*Only'Per pester-config:
New-PesterConfiguration returns a typed configuration object. Cast from a
hashtable for concise setup:
$config = [PesterConfiguration]@{
Run = @{
Path = '.\tests'
Exit = $true # non-zero exit code on failure (required for CI)
}
Filter = @{
Tag = 'Unit'
ExcludeTag = 'Slow', 'WindowsOnly'
}
Output = @{
Verbosity = 'Detailed'
}
}
Invoke-Pester -Configuration $configKey Run properties and their defaults (Run.Path, Run.ExcludePath,
Run.Exit, Run.TestExtension) are tabulated in
references/ci-and-config.md.
Per pester-coverage:
$config = New-PesterConfiguration
$config.Run.Path = '.\tests'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = '.\src'
$config.CodeCoverage.CoveragePercentTarget = 75 # fail if below 75%
$config.CodeCoverage.OutputFormat = 'JaCoCo' # or 'CoverageGutters'
$config.CodeCoverage.OutputPath = 'coverage.xml'
Invoke-Pester -Configuration $configPer pcov, Pester does not traverse directories automatically for
coverage; set CodeCoverage.Path explicitly when source files are not
co-located with tests.
Per pester-results:
$config = New-PesterConfiguration
$config.Run.Path = '.\tests'
$config.Run.Exit = $true
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml' # or 'JUnitXml'
$config.TestResult.OutputPath = 'testResults.xml'
Invoke-Pester -Configuration $configFull CI pipeline definitions - a single cross-platform GitHub Actions matrix (with code coverage and artifact upload) and the Azure DevOps Publish Test Results task - are in references/ci-and-config.md.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Dot-sourcing the script under test inside It | Re-executes on every test; slow and leaks state | Dot-source once in BeforeAll |
Using plain $var in BeforeAll, reading in It | PowerShell scope: It does not inherit BeforeAll locals | Use $script:var |
Mocking in It when needed across multiple tests | Mock scopes to that single test only | Move mock to BeforeAll or BeforeEach |
Omitting Run.Exit = $true in CI config | Pester exits 0 even on failures; CI pipeline passes on broken tests | Set Run.Exit = $true |
Setting CodeCoverage.Path = '.' without explicit paths | Pester may miss source files not co-located with tests | Set CodeCoverage.Path to the source directory explicitly |
Using legacy Invoke-Pester -Script syntax | Removed in Pester v5; breaks silently on older runners | Use PesterConfiguration with Run.Path |
Describe). Code written for v4 may need
mock placement adjusted.pwsh processes or using a CI matrix.bats-testing is more idiomatic.$PesterBoundParameters) require a minimum Pester patch level; pin the
version in CI with Install-Module -Name Pester -RequiredVersion 5.x.y.pwsh install.BeforeAll, Describe/Context/It, Should.Mock, ParameterFilter, Should -Invoke, scope rules,
$PesterBoundParameters.New-PesterConfiguration, Run, Filter, Output,
Invoke-Pester -Configuration.CodeCoverage.Enabled, CoveragePercentTarget, formats,
path scoping.TestResult.Enabled, OutputFormat, OutputPath, Azure
DevOps / AppVeyor CI notes.Run property table.bats-testing - use for shell script /
Unix binary testing; Pester is the correct choice when the subject or
test is PowerShell.cli-output-conventions - what
to assert on (stable formats, exit codes, stderr vs stdout contracts).