pester
PowerShell TDD testing framework guidance for Pester v5+. Use when writing, structuring, or debugging PowerShell unit tests; mocking cmdlets, native commands (bash, git, curl), or .NET types; isolating tests with TestDrive/TestRegistry; capturing output streams; generating code coverage or JUnit/NUnit reports for CI/CD; running parameterized or tagged tests; or troubleshooting Pester Discovery vs Run phase issues.
When & Why to Use This Skill
This Claude skill provides comprehensive guidance for Pester v5+, the industry-standard TDD and unit testing framework for PowerShell. It helps developers write robust tests, implement effective mocking for cmdlets and native commands, manage test isolation using TestDrive, and integrate automated testing into CI/CD pipelines with detailed reporting and code coverage metrics.
Use Cases
- Case 1: Writing and structuring unit tests for PowerShell modules using the Pester v5+ Discovery and Run execution model to ensure code reliability.
- Case 2: Mocking complex dependencies such as external APIs, .NET types, or native CLI tools (e.g., git, curl) to create isolated and predictable test environments.
- Case 3: Automating quality assurance in CI/CD pipelines by generating standardized JUnit/NUnit test reports and JaCoCo code coverage metrics for automated feedback.
- Case 4: Troubleshooting common Pester pitfalls, such as variable scoping issues between Discovery and Run phases or state leakage between tests using TestDrive and TestRegistry.
| name | pester |
|---|---|
| description | PowerShell TDD testing framework guidance for Pester v5+. Use when writing, structuring, or debugging PowerShell unit tests; mocking cmdlets, native commands (bash, git, curl), or .NET types; isolating tests with TestDrive/TestRegistry; capturing output streams; generating code coverage or JUnit/NUnit reports for CI/CD; running parameterized or tagged tests; or troubleshooting Pester Discovery vs Run phase issues. |
Pester Unit Testing for PowerShell
Pester is PowerShell's ubiquitous test and mock framework. Pester 5+ uses a two-phase execution model (Discovery → Run) that requires specific patterns for reliable tests.
TDD Cycle
- Red – Write a failing test describing expected behavior
- Green – Implement minimal code to pass
- Refactor – Clean up while keeping tests green
Test File Structure
Test files use *.Tests.ps1 naming convention. Place alongside source files:
src/
├── Get-Widget.ps1
└── Get-Widget.Tests.ps1
Basic Template
BeforeAll {
. $PSCommandPath.Replace('.Tests.ps1', '.ps1')
}
Describe 'Get-Widget' {
Context 'when called with valid ID' {
It 'returns widget object' {
$result = Get-Widget -Id 42
$result.Id | Should -Be 42
}
}
Context 'when widget does not exist' {
It 'throws not found error' {
{ Get-Widget -Id 9999 } | Should -Throw -ErrorId 'WidgetNotFound'
}
}
}
Block Hierarchy
| Block | Purpose | Scope |
|---|---|---|
Describe |
Top-level grouping (1 per function/feature) | Container |
Context |
Scenario grouping ("when X", "with Y") | Sub-container |
It |
Single test case with assertions | Test |
BeforeAll |
Run once before all tests in block | Setup |
BeforeEach |
Run before each It |
Per-test setup |
AfterEach |
Run after each It (guaranteed) |
Per-test cleanup |
AfterAll |
Run once after all tests (guaranteed) | Final cleanup |
Discovery vs Run Phase (Critical)
Pester 5 executes in two phases:
- Discovery – Scans to find all tests (does NOT run
Itblocks) - Run – Executes tests with setup/teardown
Rule: Put ALL code inside It, BeforeAll, BeforeEach, AfterEach, AfterAll, or BeforeDiscovery.
# ❌ WRONG - runs during Discovery, $data is null in Run phase
$data = Get-ExpensiveData
Describe 'Tests' {
It 'works' { $data | Should -Not -BeNull } # FAILS!
}
# ✅ CORRECT - use BeforeAll
Describe 'Tests' {
BeforeAll { $script:data = Get-ExpensiveData }
It 'works' { $script:data | Should -Not -BeNull }
}
For dynamic test generation, use BeforeDiscovery:
BeforeDiscovery {
$testCases = @('file1.ps1', 'file2.ps1')
}
Describe 'Validate <_>' -ForEach $testCases {
BeforeAll { $file = $_ }
It 'has valid syntax' { ... }
}
Mocking
Mock any PowerShell command within test scope:
Describe 'Send-Report' {
BeforeAll {
Mock Send-MailMessage {}
Mock Get-Date { return [DateTime]'2024-01-15' }
}
It 'sends email with correct subject' {
Send-Report -Title 'Summary'
Should -Invoke Send-MailMessage -Times 1 -ParameterFilter {
$Subject -like '*Summary*'
}
}
}
Parameter Filters
Create conditional mocks for different inputs:
Mock Get-Service { @{ Status = 'Running' } } -ParameterFilter { $Name -eq 'BITS' }
Mock Get-Service { @{ Status = 'Stopped' } } -ParameterFilter { $Name -eq 'Spooler' }
Mock Get-Service { @{ Status = 'Unknown' } } # Default fallback
Mocking Native Commands (bash, git, curl)
Native commands work via $args:
Describe 'Git Operations' {
BeforeAll { Mock git { 'mocked-output' } }
It 'calls git with correct args' {
Invoke-GitPush -Branch 'main'
Should -Invoke git -ParameterFilter {
$args[0] -eq 'push' -and $args[1] -eq 'origin'
}
}
}
Module Internals
Use -ModuleName for functions inside modules:
Mock Get-InternalData { 'mocked' } -ModuleName MyModule
Use InModuleScope for private/non-exported functions:
InModuleScope MyModule {
Mock Write-Log {}
Invoke-PrivateFunction
Should -Invoke Write-Log
}
Test Isolation
TestDrive (Filesystem)
Temporary PSDrive auto-cleaned per block:
Describe 'File Processing' {
BeforeAll {
Set-Content 'TestDrive:\config.json' -Value '{"key":"value"}'
}
It 'reads config' {
$cfg = Get-Content 'TestDrive:\config.json' | ConvertFrom-Json
$cfg.key | Should -Be 'value'
}
}
Use $TestDrive for .NET APIs requiring full paths:
$path = Join-Path $TestDrive 'file.txt'
[System.IO.File]::WriteAllText($path, 'content')
TestRegistry (Windows)
Temporary registry hive:
BeforeAll {
New-Item -Path 'TestRegistry:\MyApp'
New-ItemProperty -Path 'TestRegistry:\MyApp' -Name 'Setting' -Value 'Test'
}
Environment Variables
Save and restore manually:
BeforeEach {
$script:oldEnv = $env:MY_VAR
$env:MY_VAR = 'test-value'
}
AfterEach {
$env:MY_VAR = $script:oldEnv
}
Output Capture
Stream Redirection
| Stream | Command | Capture |
|---|---|---|
| 1 (Success) | Write-Output | Direct assignment |
| 2 (Error) | Write-Error | 2>&1 or -ErrorVariable |
| 3 (Warning) | Write-Warning | 3>&1 |
| 4 (Verbose) | Write-Verbose | 4>&1 with -Verbose |
| 6 (Information) | Write-Host | 6>&1 |
It 'captures Write-Host' {
$result = MyFunction 6>&1
$result | Should -Contain 'expected message'
}
ANSI Color Stripping
function Remove-AnsiCodes {
param([string]$Text)
$Text -replace '\x1b\[[0-9;]*[a-zA-Z]', ''
}
$clean = Remove-AnsiCodes $coloredOutput
Or configure Pester: $config.Output.RenderMode = 'Plaintext'
Parameterized Tests
Use -ForEach or -TestCases:
Describe 'Add-Numbers' {
It 'adds <a> + <b> = <expected>' -TestCases @(
@{ a = 2; b = 3; expected = 5 }
@{ a = -1; b = 1; expected = 0 }
) {
Add-Numbers $a $b | Should -Be $expected
}
}
Running Specific Tests
Tags
It 'slow test' -Tag 'Integration', 'Slow' { ... }
# Run only tagged tests
Invoke-Pester -TagFilter 'Unit' -ExcludeTagFilter 'Slow'
Name Filters
Invoke-Pester -FullNameFilter '*Get-Widget*returns*'
Skip
It 'admin only' -Skip:(-not (Test-IsAdmin)) { ... }
Code Coverage
$config = New-PesterConfiguration
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './src'
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = 'coverage.xml'
$config.CodeCoverage.CoveragePercentTarget = 80
Invoke-Pester -Configuration $config
CI Reports (JUnit/NUnit)
$config = New-PesterConfiguration
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'JUnitXml' # or NUnitXml
$config.TestResult.OutputPath = 'test-results.xml'
$config.Run.Exit = $true # Exit code for CI
Invoke-Pester -Configuration $config
Additional Resources
- references/anti-patterns.md - Common mistakes and pitfalls with solutions
- references/mocking-patterns.md - Advanced mocking scenarios (APIs, databases, native commands)
- references/ci-integration.md - GitHub Actions, Azure DevOps, GitLab CI, Jenkins examples
Common Anti-Patterns
See references/anti-patterns.md for detailed examples.
Quick checklist:
- ❌ Code outside Pester blocks
- ❌ Tests depending on each other
- ❌ Using
foreachinstead of-ForEach - ❌ Mocking the function under test
- ❌ Over-specifying mock interactions
- ❌ Global variables in tests
Assertion Quick Reference
| Assertion | Description |
|---|---|
Should -Be |
Case-insensitive equality |
Should -BeExactly |
Case-sensitive equality |
Should -BeTrue / -BeFalse |
Boolean |
Should -BeNullOrEmpty |
Null/empty check |
Should -BeOfType |
Type checking |
Should -Contain |
Collection contains |
Should -Match |
Regex (case-insensitive) |
Should -BeLike |
Wildcard match |
Should -Throw |
Exception expected |
Should -Exist |
Path exists |
Should -HaveCount |
Collection count |
Should -Invoke |
Mock was called |
Full assertion list: Get-ShouldOperator