PowerShell Pester: Write Unit Tests for Your Scripts

PowerShell Pester: Write Unit Tests for Your Scripts

PowerShell Tips Editor 3 min read
PowerShell Pester: Write Unit Tests for Your Scripts

Scripts that work today break after a module update or a refactor next month. Without automated tests, you discover these regressions in production. PowerShell Pester testing is the solution: a unit test framework built for PowerShell that runs before deployment, catches broken functions, and gives you confidence that your changes do not silently break existing behavior. This post covers Pester 5 syntax, assertions, mocking, and CI integration.

Quick Answer / TL;DR

Install Pester 5 with Install-Module Pester -Force -SkipPublisherCheck. Write tests in a file named FunctionName.Tests.ps1. Run with Invoke-Pester -Path .\Tests.

Install and Configure Pester 5

Windows ships with Pester version 3, which has significantly different syntax. Always install Pester 5 from the PowerShell Gallery for new projects. Use -Force -SkipPublisherCheck to bypass the publisher mismatch with the built-in version.

# Install Pester 5 (overrides built-in Pester 3)
Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser

# Verify version
Import-Module Pester -Force
(Get-Module Pester).Version
Major Minor Build Revision
----- ----- ----- --------
5     6     1     0

Describe and It Blocks

Tests are organized in Describe blocks (one per function or feature) containing It blocks (one per test case). Use BeforeAll for setup that runs once per Describe block and BeforeEach for setup before every It. The function under test is dot-sourced in BeforeAll so it is available throughout the block.

# File: Get-ServerStatus.Tests.ps1
BeforeAll {
    . "$PSScriptRoot\Get-ServerStatus.ps1"
}

Describe 'Get-ServerStatus' {

    It 'Returns an object with Name property' {
        $result = Get-ServerStatus -Name 'web01'
        $result.Name | Should -Be 'web01'
    }

    It 'Returns Status property' {
        $result = Get-ServerStatus -Name 'web01'
        $result.Status | Should -Not -BeNullOrEmpty
    }

    It 'Throws when Name is empty' {
        { Get-ServerStatus -Name '' } | Should -Throw
    }
}

Should Assertions

Pester 5 uses a fluent Should assertion syntax. The most commonly used assertions are:

  • Should -Be 'value' — exact equality
  • Should -BeExactly 'Value' — case-sensitive equality
  • Should -BeTrue / Should -BeFalse
  • Should -BeNullOrEmpty / Should -Not -BeNullOrEmpty
  • Should -Throw — verifies a script block throws an exception
  • Should -Contain 'item' — collection contains value
  • Should -Match 'pattern' — regex match
Describe 'Should assertions demo' {
    It 'String equality' {
        'PowerShell' | Should -Be 'PowerShell'
    }

    It 'Numeric comparison' {
        42 | Should -BeGreaterThan 10
        42 | Should -BeLessThan 100
    }

    It 'Collection membership' {
        @('a', 'b', 'c') | Should -Contain 'b'
    }

    It 'Exception thrown' {
        { 1 / 0 } | Should -Throw
    }

    It 'Null check' {
        $null | Should -BeNullOrEmpty
        ''    | Should -BeNullOrEmpty
    }
}

Mock External Commands

Mock replaces a cmdlet or function with a controlled stub for the duration of the test. This prevents tests from making real network calls, writing files, or modifying system state. Mocks are scoped to their enclosing Describe or It block.

Describe 'Send-Alert function' {
    BeforeAll {
        . "$PSScriptRoot\Send-Alert.ps1"
    }

    It 'Calls Invoke-RestMethod when alert is critical' {
        Mock Invoke-RestMethod { return @{ status = 'ok' } }

        Send-Alert -Message 'Disk full' -Severity 'Critical'

        Should -Invoke Invoke-RestMethod -Times 1 -Exactly
    }

    It 'Does not call Invoke-RestMethod for info alerts' {
        Mock Invoke-RestMethod {}

        Send-Alert -Message 'Script started' -Severity 'Info'

        Should -Invoke Invoke-RestMethod -Times 0
    }
}

Test a Real Function

A complete test file for a function that converts file sizes. The test covers expected output, edge cases, and error conditions — the three categories every function test suite should address.

# File: ConvertTo-HumanSize.Tests.ps1
BeforeAll {
    function ConvertTo-HumanSize {
        param([long]$Bytes)
        switch ($Bytes) {
            { $_ -ge 1GB } { return "$([math]::Round($_ / 1GB, 2)) GB" }
            { $_ -ge 1MB } { return "$([math]::Round($_ / 1MB, 2)) MB" }
            { $_ -ge 1KB } { return "$([math]::Round($_ / 1KB, 2)) KB" }
            default        { return "$_ B" }
        }
    }
}

Describe 'ConvertTo-HumanSize' {
    It 'Returns bytes for small values' {
        ConvertTo-HumanSize -Bytes 512 | Should -Be '512 B'
    }

    It 'Converts kilobytes correctly' {
        ConvertTo-HumanSize -Bytes 2048 | Should -Be '2 KB'
    }

    It 'Converts megabytes correctly' {
        ConvertTo-HumanSize -Bytes 5MB | Should -Match 'MB$'
    }

    It 'Converts gigabytes correctly' {
        ConvertTo-HumanSize -Bytes 1GB | Should -Be '1 GB'
    }
}

Run Tests in GitHub Actions CI

Add Pester to your CI pipeline to block merges when tests fail. The example below installs Pester, runs all test files, and exits with a non-zero code if any tests fail — causing the CI job to fail.

# .github/workflows/test.yml equivalent PowerShell step
# In your CI pipeline script or workflow:

Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser

$config = New-PesterConfiguration
$config.Run.Path = '.\Tests'
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'TestResults.xml'
$config.TestResult.OutputFormat = 'JUnitXml'

$result = Invoke-Pester -Configuration $config -PassThru

if ($result.FailedCount -gt 0) {
    Write-Error "Pester: $($result.FailedCount) test(s) failed"
    exit 1
}
Write-Host "All $($result.PassedCount) tests passed"

Common Errors and Fixes

  • Pester 4 vs 5 syntax — major breaking changes between versions. Pester 5 changed assertion syntax (Should -Be instead of Should Be), mock syntax (Should -Invoke instead of Assert-MockCalled), and configuration approach. If you see “Should: Parameter set cannot be resolved”, you are mixing syntax versions. Always specify Import-Module Pester -MinimumVersion 5.0 at the top of test files.
  • Mock scope limited to Describe block unless BeforeAll used. Mocks defined inside an It block are scoped only to that test. Mocks in BeforeAll are available to all It blocks in the parent Describe. Failing to understand scope is the most common cause of mock-related test failures.

Related Cmdlets / See Also

Wrapping Up

Pester transforms PowerShell scripting from a “deploy and hope” workflow to a tested, confident process. Write tests alongside every function, mock external dependencies to keep tests fast and isolated, and integrate with CI to block untested or broken code from reaching production. Start with three tests per function: happy path, edge case, and error condition.

Send-Item -To