Files
awesome-copilot/skills/pester-migration/references/v5-to-v6.md
T
Gijs Reijn b6a84a833b Refine Pester migration skill by removing release candidate (#2278)
* Refine Pester migration skill descriptions and remove release candidate notes

* Take Copilot remark
2026-07-13 09:56:54 +10:00

10 KiB

Migrating Pester v5 → v6

Pester 6 builds on v5 and is largely backwards-compatible — most v5 suites run on v6 with no changes, and your existing Should -Be assertions keep working. The work is fixing a handful of previously-deprecated behaviors that now throw. This is a low-to-medium effort jump.

Official guide: https://pester.dev/docs/migrations/v5-to-v6


Quick upgrade checklist

For most suites this is low risk. Run through these, then read the details for anything that bites:

  1. Run on Windows PowerShell 5.1 or PowerShell 7.4+ (older PS is dropped).
  2. Check any -ForEach/-TestCases that can be empty — it now throws unless you add -AllowNullOrEmptyForEach.
  3. Remove duplicate BeforeAll/BeforeEach/AfterAll/AfterEach in the same block.
  4. Replace Assert-MockCalled / Assert-VerifiableMock with Should -Invoke / Should -InvokeVerifiable.
  5. Add a default Mock where some calls don't match a -ParameterFilter — mocks no longer fall through to the real command.
  6. If you call Invoke-Pester with v4-style params (-Script, -OutputFile, …), switch to New-PesterConfiguration.

Breaking changes (symptom → fix)

PowerShell 5.1 and 7.4+ only

PowerShell 3, 4, 6, and early/unsupported 7 are dropped (all out of support from Microsoft), which let Pester move its C# to .NET 8 (net462 for Windows PowerShell 5.1).

  • Symptom: Pester won't import on an older PowerShell.
  • Fix: Update your machines and CI agents to Windows PowerShell 5.1 or PowerShell 7.4+.

Discovery and Run now happen per file

In v5 the run had two global phases: discover every file, then run every file. In v6 the unit of work is a single file — Pester discovers a file and runs it before moving to the next. This is what enables the experimental parallel runner, and serial runs follow the same model.

  • Symptom: a file that relied on another file's discovery-time side effect fails — e.g. a module imported at the top of one file, a global variable, a changed working directory, or -ForEach data defined in a different file.
  • Fix: make each test file self-contained. Do discovery-time setup in BeforeDiscovery, and import the modules the file needs in its own BeforeAll. For setup every file needs, use Run.BeforeContainer (config) or a Pester.BeforeContainer.ps1 at the repo root.
BeforeDiscovery {
    $cases = Get-Content "$PSScriptRoot/cases.json" | ConvertFrom-Json
}
Describe 'MyModule' {
    BeforeAll { Import-Module "$PSScriptRoot/MyModule.psm1" }
    It 'handles <Name>' -ForEach $cases { Invoke-Thing $Name | Should -Be 'ok' }
}

On-screen output also changes: one Running tests from N files. banner, per-file results, then a single grand total. The old Starting discovery in N files. / Discovery found X tests framing is gone for a normal run. The parallel runner (Run.Parallel) keeps the same result object with a few edge cases — see https://pester.dev/docs/usage/result-object#parallel-runner-edge-cases.

Empty or $null -ForEach throws

-ForEach (or -TestCases) given $null or @() now throws instead of silently skipping. This catches the common bug of pointing -ForEach at a variable that wasn't defined in BeforeDiscovery, or external data that failed to load.

  • Symptom:
    Value can not be null or empty array. If this is expected, use -AllowNullOrEmptyForEach
    on this Describe, or set the Run.FailOnNullOrEmptyForEach configuration option to $false ...
    
  • Fix: when the data can legitimately be empty, allow it on that specific block/test:
    Describe 'Optional cases' -ForEach $cases -AllowNullOrEmptyForEach {
        It 'runs only when there is data' { }
    }
    
    You can disable the check for the whole run with Run.FailOnNullOrEmptyForEach = $false, but that brings back the silent skipping it's meant to catch — prefer fixing the data or using -AllowNullOrEmptyForEach where empty is genuinely expected.

Duplicate setup/teardown blocks throw

A block may have only one of each BeforeAll/BeforeEach/AfterAll/AfterEach. Two of the same (a common copy-paste bug) was silently allowed in v5; v6 throws.

  • Symptom: BeforeAll is already defined in this block. Each block can only have one BeforeAll.
  • Fix: combine them into one block:
    Describe 'd' {
        BeforeAll {
            $a = 1
            $b = 2   # was a second BeforeAll
        }
    }
    

Test names evaluate <...> templates as expressions

In v6 the content of every <...> token in a Describe/Context/It name is evaluated as a PowerShell expression in the test's run scope (current -ForEach item and its properties, in-scope variables, arithmetic, method calls). Everything outside <...> is kept literal. In v5 only simple data/variable/property references were substituted.

  • Symptom: a name containing expression-like content inside <...> that used to render literally now evaluates.
    # v5 renders literally: "adds up to <($a + $b)>"; v6 evaluates: "adds up to 3"
    It 'adds up to <($a + $b)>' -ForEach @(@{ a = 1; b = 2 }) { }
    
  • Fix (to keep the literal text): escape the leading < with a backtick:
    It 'adds up to `<($a + $b)`>' -ForEach @(@{ a = 1; b = 2 }) { }
    

Assert-MockCalled and Assert-VerifiableMock removed

Deprecated in v5, removed in v6.

  • Symptom: The term 'Assert-MockCalled' is not recognized ...
  • Fix:
    # Assert-MockCalled     -> Should -Invoke
    # Assert-VerifiableMock -> Should -InvokeVerifiable
    Should -Invoke Get-Thing -Times 1 -Exactly
    Should -InvokeVerifiable
    

Mocks no longer fall through to the real command

In v5, a call to a mocked command that matched none of your -ParameterFilter mocks quietly ran the real command. v6 removes that implicit fall-through.

  • Symptom:
    No mock for command 'Get-Thing' matched the call: none of the parameter filters matched,
    and there is no default mock to fall back to. Add a default mock ...
    
  • Fix: add a default mock (no -ParameterFilter) for the unfiltered calls, or widen a filter:
    Mock Get-Thing -MockWith { 'default' }                                # everything else
    Mock Get-Thing -ParameterFilter { $Name -eq 'a' } -MockWith { 'a' }   # special case
    

Set-ItResult -Pending removed

Pending was never fully implemented in v5 and is gone.

  • Symptom: Parameter set cannot be resolved using the specified named parameters.
  • Fix: use -Inconclusive or -Skipped, or mark the test with It … -Skip:
    Set-ItResult -Inconclusive -Because 'not implemented yet'
    

Code coverage uses the Profiler tracer by default

Coverage no longer sets a breakpoint on every command; it uses the Profiler's tracer, which is much faster on large code bases. CodeCoverage.UseBreakpoints is no longer experimental and defaults to $false.

  • Symptom: coverage numbers differ from v5 and you want the old behavior.
  • Fix: $config.CodeCoverage.UseBreakpoints = $true.

CodeCoverage.OutputFormat = 'CoverageGutters' removed

All coverage output is now relative to the repo root (Run.RepoRoot, found from the .git directory), so plain JaCoCo already works with the Coverage Gutters extension.

  • Symptom: setting OutputFormat to 'CoverageGutters' throws an invalid-value error.
  • Fix: use JaCoCo (default) or Cobertura.

Invoke-Pester legacy (v4) parameters removed

Only the Simple set (-Path, -Output, -Container, -Tag, …) and the Advanced set (-Configuration) remain. The v4-style parameter set is gone.

  • Symptom: calls like Invoke-Pester -Script … -OutputFile … -OutputFormat … -EnableExit -CodeCoverage … fail with a parameter-binding error.
  • Fix: use a configuration object (the full parameter→config map is in the "Invoke-Pester parameters → New-PesterConfiguration" section of v4-to-v5.md):
    $config = New-PesterConfiguration
    $config.Run.Path = './tests'
    $config.Run.Exit = $true
    $config.TestResult.Enabled  = $true
    $config.TestResult.OutputPath = 'result.xml'
    $config.TestResult.OutputFormat = 'NUnitXml'
    $config.CodeCoverage.Enabled = $true
    $config.CodeCoverage.Path = './src'
    Invoke-Pester -Configuration $config
    

Config convenience in v6: setting any non-default option in the TestResult or CodeCoverage section auto-enables that section, so you no longer have to set .Enabled = $true separately for a report to be written.


New Should-* assertions — optional, NOT part of upgrading

v6 adds a new family of assertions — Should-Be, Should-Throw, Should-Invoke, and ~40 more (note the hyphen: Should-Be, a distinct command, vs. the classic Should -Be) — with clearer failure messages. You do not need to touch your existing Should -Be assertions to upgrade; they keep working. Treat any Should -BeShould-Be rewrite as a separate, later effort, not part of this migration. Reference: https://pester.dev/docs/commands/Should-Be.

If you do try them: the new assertions take the actual value from the pipeline or -Actual. The pipeline unwraps input (so @(1) becomes 1, @() becomes $null, and a collection is re-collected as [object[]], losing types like [int[]]). Use -Actual when you need the exact value or concrete collection type.


v5 → v6 checklist

  • Suite runs green on v5 first (baseline).
  • Running on Windows PowerShell 5.1 or PowerShell 7.4+.
  • -ForEach/-TestCases that can be empty marked -AllowNullOrEmptyForEach (or data fixed).
  • Duplicate Before*/After* blocks combined.
  • Assert-MockCalledShould -Invoke; Assert-VerifiableMockShould -InvokeVerifiable.
  • Default Mock added wherever calls can miss every -ParameterFilter.
  • Set-ItResult -Pending-Inconclusive/-Skipped.
  • <...> test-name templates that should stay literal are backtick-escaped.
  • Each test file is self-contained (no reliance on another file's discovery-time state).
  • Invoke-Pester legacy params → New-PesterConfiguration.
  • Coverage OutputFormat is JaCoCo/Cobertura; UseBreakpoints set only if old numbers needed.
  • Suite green on v6; diff reviewed; committed.