Pester Testing Instructions

June 26, 2026 · View on GitHub

Pester 5.x is the testing framework for all PowerShell code. Run tests through the repository's test runner (check package.json for a test script, or invoke Invoke-Pester directly if no runner is configured). Follow the repository's conventions for test execution.

Test File Naming

Test files use a .Tests.ps1 suffix matching the production file name:

Production fileTest file
Test-DependencyPinning.ps1Test-DependencyPinning.Tests.ps1
SecurityHelpers.psm1SecurityHelpers.Tests.ps1
SecurityClasses.psm1SecurityClasses.Tests.ps1

Test File Location

Mirror directory pattern: Place test files in a tests/ directory that mirrors the production directory layout. Each production subdirectory has a corresponding test subdirectory. Discover the repository's test directory structure by examining existing test files.

Co-located tests: Self-contained packages (such as skills or plugins) place tests inside the package directory rather than the mirror tree:

package-root/
├── scripts/
│   ├── action.ps1
│   └── helpers.psm1
└── tests/
    ├── action.Tests.ps1
    └── helpers.Tests.ps1

Test File Header

Test files place #Requires -Modules Pester before the copyright header. This ordering differs from production scripts:

#Requires -Modules Pester
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT

SUT Import Patterns

Import the system under test in a file-level BeforeAll block. Use the pattern matching the production file type:

Dot-source for scripts (.ps1):

BeforeAll {
    . (Join-Path $PSScriptRoot '../../security/Test-DependencyPinning.ps1')
}

Import-Module for modules (.psm1):

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '../../security/Modules/SecurityHelpers.psm1') -Force
}

using module for class modules (parse-time type resolution):

using module ..\..\security\Modules\SecurityClasses.psm1

The using module statement appears at the top of the file outside any block because PowerShell processes it at parse time.

BeforeAll Setup

File-level BeforeAll initializes the test environment. Common activities include SUT import, mock module import, fixture path resolution, and output suppression:

BeforeAll {
    . (Join-Path $PSScriptRoot '../../security/Test-DependencyPinning.ps1')
    Import-Module (Join-Path $PSScriptRoot '../../security/Modules/SecurityHelpers.psm1') -Force
    Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force
    $script:FixtureRoot = Join-Path $PSScriptRoot '../fixtures/Security'
    Mock Write-Host {}
    Mock Write-CIAnnotation {} -ModuleName SecurityHelpers
}

Describe, Context, and It Blocks

All Describe blocks require -Tag 'Unit'. The pester configuration excludes Integration and Slow tags by default:

Describe 'FunctionName' -Tag 'Unit' {
    Context 'when input is valid' {
        It 'Returns expected output' {
            Get-Something -Path 'test' | Should -Be 'result'
        }
    }
}

Context groups related scenarios. Each It tests a single behavior with a descriptive sentence name.

Data-Driven Tests

Use -ForEach on It blocks for parameterized testing:

It 'Accepts valid type <Value>' -ForEach @(
    @{ Value = 'Unpinned' }
    @{ Value = 'Stale' }
    @{ Value = 'VersionMismatch' }
) {
    $v = [DependencyViolation]::new()
    $v.ViolationType = $Value
    $v.ViolationType | Should -Be $Value
}

Mock Patterns

Output suppression: Empty scriptblock mocks prevent console noise:

Mock Write-Host {}

Module-scoped mocks: -ModuleName injects mocks into modules under test:

Mock Write-CIAnnotation {} -ModuleName SecurityHelpers

Parameter-filtered mocks: -ParameterFilter targets specific invocations:

Mock git {
    $global:LASTEXITCODE = 0
    return 'abc123'
} -ModuleName LintingHelpers -ParameterFilter {
    $args[0] -eq 'merge-base'
}

Mock Verification

Use Should -Invoke to verify mock calls:

Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -Times 1 -Exactly
Should -Invoke Write-CIAnnotation -ModuleName SecurityHelpers -ParameterFilter {
    $Level -eq 'Warning'
}

Test Isolation

$TestDrive: Pester-managed temp directory, automatically cleaned per Describe:

$testDir = Join-Path $TestDrive 'test-collection'
New-Item -ItemType Directory -Path $testDir -Force

New-TemporaryFile with try/finally: Manual temp file management when $TestDrive is insufficient:

$tempFile = New-TemporaryFile
try {
    # test using $tempFile
}
finally {
    Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
}

$script: scope: Shares state across It blocks within a Describe or Context:

BeforeAll {
    $script:result = Get-Something -Path 'test'
}
It 'Returns correct name' {
    $script:result.Name | Should -Be 'expected'
}

Environment Save and Restore

Tests modifying environment variables use the GitMocks.psm1 save/restore pattern:

BeforeAll {
    Import-Module "$PSScriptRoot/../Mocks/GitMocks.psm1" -Force
}
BeforeEach {
    Save-CIEnvironment
    $script:MockFiles = Initialize-MockCIEnvironment
}
AfterEach {
    Remove-MockCIFiles -MockFiles $script:MockFiles
    Restore-CIEnvironment
}

Cleanup

Remove imported modules in AfterAll to prevent state leakage between test files:

AfterAll {
    Remove-Module SecurityHelpers -Force -ErrorAction SilentlyContinue
    Remove-Module GitMocks -Force -ErrorAction SilentlyContinue
}

Assertion Reference

AssertionUsage
Should -BeExact value equality
Should -BeExactlyCase-sensitive equality
Should -BeTrue / -BeFalseBoolean checks
Should -BeNullOrEmptyNull or empty string
Should -Not -BeNullOrEmptyNon-null and non-empty
Should -MatchRegex matching
Should -BeLikeWildcard matching
Should -ContainCollection membership
Should -BeOfTypeType assertion
Should -HaveCountCollection length
Should -ThrowException expected
Should -Not -ThrowNo exception expected
Should -BeGreaterThanNumeric comparison
Should -BeLessThanNumeric comparison
Should -InvokeMock call verification

Running Tests

Check package.json for a test runner script (common name: test:ps). If a runner is configured, use it to execute tests:

# Example: run all tests via the repository's test runner
npm run test:ps

# Example: run tests for a specific directory or file
npm run test:ps -- -TestPath "path/to/tests/"

If no runner is configured, invoke Pester directly:

Invoke-Pester -Path './tests/' -Output Detailed

After execution, check the repository's log or output directory for structured test results (such as summary and failure JSON files) when available.

Complete Test Example

#Requires -Modules Pester
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT

BeforeAll {
    . (Join-Path $PSScriptRoot '../../linting/Invoke-Linter.ps1')
    Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force
    $script:FixtureRoot = Join-Path $PSScriptRoot '../fixtures/Linting'
    Mock Write-Host {}
}

Describe 'Invoke-Linter' -Tag 'Unit' {
    Context 'when input file is valid' {
        BeforeAll {
            $script:result = Invoke-Linter -Path (Join-Path $script:FixtureRoot 'valid.md')
        }

        It 'Returns zero violations' {
            $script:result.Violations | Should -HaveCount 0
        }

        It 'Sets status to pass' {
            $script:result.Status | Should -Be 'Pass'
        }
    }

    Context 'when input file has errors' {
        BeforeAll {
            $script:result = Invoke-Linter -Path (Join-Path $script:FixtureRoot 'invalid.md')
        }

        It 'Returns violations' {
            $script:result.Violations | Should -Not -BeNullOrEmpty
        }

        It 'Includes file path in each violation' {
            $script:result.Violations | ForEach-Object {
                $_.File | Should -Not -BeNullOrEmpty
            }
        }
    }
}

AfterAll {
    Remove-Module GitMocks -Force -ErrorAction SilentlyContinue
}