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 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.
Test files use *.Tests.ps1 naming convention. Place alongside source files:
src/
├── Get-Widget.ps1
└── Get-Widget.Tests.ps1
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 | 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 |
Pester 5 executes in two phases:
It blocks)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' { ... }
}
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*'
}
}
}
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
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'
}
}
}
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
}
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')
Temporary registry hive:
BeforeAll {
New-Item -Path 'TestRegistry:\MyApp'
New-ItemProperty -Path 'TestRegistry:\MyApp' -Name 'Setting' -Value 'Test'
}
Save and restore manually:
BeforeEach {
$script:oldEnv = $env:MY_VAR
$env:MY_VAR = 'test-value'
}
AfterEach {
$env:MY_VAR = $script:oldEnv
}
| 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'
}
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'
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
}
}
It 'slow test' -Tag 'Integration', 'Slow' { ... }
# Run only tagged tests
Invoke-Pester -TagFilter 'Unit' -ExcludeTagFilter 'Slow'
Invoke-Pester -FullNameFilter '*Get-Widget*returns*'
It 'admin only' -Skip:(-not (Test-IsAdmin)) { ... }
$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
$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
See references/anti-patterns.md for detailed examples.
Quick checklist:
foreach instead of -ForEach| 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
npx skills add OleksandrKucherenko/Pester 测试框架下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer