pester

Pester Unit Testing for PowerShell

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "pester" with this command: npx skills add oleksandrkucherenko/e-bash/oleksandrkucherenko-e-bash-pester

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 It blocks)

  • 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-Widgetreturns*'

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 foreach instead 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

skill-learning-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

elegant-code

No summary provided by upstream source.

Repository SourceNeeds Review
General

Pantry

Pantry — a fast home management tool. Log anything, find it later, export when needed.

Registry SourceRecently Updated