PowerShell Testing with Pester and PSScriptAnalyzer
This skill provides expertise in PowerShell testing using the Pester framework and code quality analysis with PSScriptAnalyzer.
Pester Testing Framework
Test Structure
Use the standard Pester structure with Describe, Context, and It blocks:
Describe "FunctionName" {
Context "When specific condition exists" {
It "Should produce expected behavior" {
# Arrange
$expected = "value"
# Act
$result = FunctionName -Parameter "value"
# Assert
$result | Should -Be $expected
}
}
}
Setup and Teardown
Use lifecycle hooks for test setup and cleanup:
BeforeAll {
# Runs once before all tests in the block
. $PSScriptRoot/../script.ps1
}
BeforeEach {
# Runs before each test
$testData = @{ Key = "Value" }
}
AfterEach {
# Runs after each test
Remove-Item -Path $testPath -ErrorAction SilentlyContinue
}
AfterAll {
# Runs once after all tests in the block
# Cleanup resources
}
Pester Assertions
Common assertion patterns:
- •
Should -Be- Exact equality - •
Should -BeExactly- Case-sensitive string equality - •
Should -Match- Regex pattern matching - •
Should -Contain- Collection contains item - •
Should -BeNullOrEmpty- Null or empty check - •
Should -Throw- Exception testing - •
Should -Exist- File/path existence - •
Should -BeOfType- Type checking - •
Should -Invoke- Mock verification (with-Times,-Exactly,-ParameterFilter)
Mocking
Mock external dependencies to isolate tests:
BeforeAll {
Mock Get-Content { return "mocked content" }
Mock Get-ChildItem {
return @(
[PSCustomObject]@{ Name = "file1.txt"; Length = 100 }
)
} -ParameterFilter { $Path -eq "C:\temp" }
}
It "Should call Get-Content" {
$result = Get-SomeData
Should -Invoke Get-Content -Times 1 -Exactly
}
Test Organization
- •Name test files with
.Tests.ps1suffix - •Group related tests in the same
Describeblock - •Use
Contextto group tests by scenario or condition - •Keep test names descriptive and action-oriented
- •One assertion per
Itblock when possible
PSScriptAnalyzer Best Practices
PSScriptAnalyzer is a static code analyzer that enforces PowerShell best practices and catches common issues.
Running PSScriptAnalyzer
# Analyze a single file Invoke-ScriptAnalyzer -Path script.ps1 # Analyze recursively with severity filter Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error,Warning # Use specific rules Invoke-ScriptAnalyzer -Path script.ps1 -IncludeRule PSAvoidUsingPositionalParameters
Critical Rules to Follow
PSAvoidUsingPositionalParameters
# Bad Get-ChildItem C:\temp *.txt # Good Get-ChildItem -Path C:\temp -Filter *.txt
PSUseShouldProcessForStateChangingFunctions
function Remove-CustomItem {
[CmdletBinding(SupportsShouldProcess)]
param($Path)
if ($PSCmdlet.ShouldProcess($Path, "Remove")) {
Remove-Item -Path $Path
}
}
PSAvoidUsingCmdletAliases
# Bad
gci | ? { $_.Length -gt 1MB } | % { $_.Name }
# Good
Get-ChildItem | Where-Object { $_.Length -gt 1MB } | ForEach-Object { $_.Name }
PSUseDeclaredVarsMoreThanAssignments
- •Ensure variables are used after assignment
- •Remove unused variables
PSAvoidUsingWriteHost
# Bad - can't be captured or redirected Write-Host "Information" # Good - proper output streams Write-Output "Information" # Success stream Write-Error "Error message" # Error stream Write-Warning "Warning" # Warning stream Write-Verbose "Details" # Verbose stream
PSAvoidUsingPlainTextForPassword
# Bad param([string]$Password) # Good param([SecureString]$Password)
Cmdlet Development Guidelines
Follow Microsoft's official cmdlet development guidelines from https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/cmdlet-development-guidelines
Naming Conventions:
- •Use approved verbs: Get, Set, New, Remove, Add, Clear, etc.
- •Use singular nouns:
Get-Process, notGet-Processes - •Follow Verb-Noun pattern
Parameter Guidelines:
- •Use
[CmdletBinding()]for advanced functions - •Support common parameters via
[CmdletBinding()] - •Use
[Parameter()]attributes to define required/optional parameters - •Use
ValueFromPipelineandValueFromPipelineByPropertyNameappropriately - •Validate parameters with
[ValidateSet()],[ValidateRange()], etc.
Integrating PSScriptAnalyzer in Tests
Describe "PSScriptAnalyzer" {
Context "Script quality" {
It "Should pass PSScriptAnalyzer rules" {
$results = Invoke-ScriptAnalyzer -Path $PSScriptRoot/../script.ps1
$results | Should -BeNullOrEmpty
}
It "Should have no high severity issues" {
$results = Invoke-ScriptAnalyzer -Path $PSScriptRoot/.. -Recurse -Severity Error
$results | Should -BeNullOrEmpty
}
}
}
Test Execution
Running Pester Tests
# Run all tests in current directory Invoke-Pester # Run specific test file Invoke-Pester -Path .\MyFunction.Tests.ps1 # Run with code coverage Invoke-Pester -CodeCoverage .\script.ps1 # Output to CI-friendly format Invoke-Pester -OutputFormat NUnitXml -OutputFile test-results.xml # Run with detailed output Invoke-Pester -Output Detailed
CI/CD Integration
For GitHub Actions or other CI systems:
$config = New-PesterConfiguration $config.Run.Path = './tests' $config.Run.Exit = $true $config.TestResult.Enabled = $true $config.TestResult.OutputPath = 'test-results.xml' $config.CodeCoverage.Enabled = $true Invoke-Pester -Configuration $config
Common Patterns
Testing Functions with Pipeline Input
It "Should accept pipeline input" {
$input = @("file1.txt", "file2.txt")
$result = $input | Process-CustomFunction
$result.Count | Should -Be 2
}
Testing Error Handling
It "Should throw when path doesn't exist" {
{ Get-CustomData -Path "C:\nonexistent" } | Should -Throw
}
It "Should write error for invalid input" {
Get-CustomData -Path "invalid" -ErrorVariable err
$err | Should -Not -BeNullOrEmpty
}
Testing Private Functions
InModuleScope MyModule {
It "Should test private function" {
PrivateFunction | Should -Be $expected
}
}
Quick Reference
When to use this skill
- •Writing new Pester tests for PowerShell scripts or modules
- •Debugging failing PowerShell tests
- •Analyzing PowerShell code quality with PSScriptAnalyzer
- •Refactoring PowerShell code to meet best practices
- •Setting up PowerShell test infrastructure
- •Integrating PowerShell tests into CI/CD pipelines