* Refine Pester migration skill descriptions and remove release candidate notes * Take Copilot remark
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:
- Run on Windows PowerShell 5.1 or PowerShell 7.4+ (older PS is dropped).
- Check any
-ForEach/-TestCasesthat can be empty — it now throws unless you add-AllowNullOrEmptyForEach. - Remove duplicate
BeforeAll/BeforeEach/AfterAll/AfterEachin the same block. - Replace
Assert-MockCalled/Assert-VerifiableMockwithShould -Invoke/Should -InvokeVerifiable. - Add a default
Mockwhere some calls don't match a-ParameterFilter— mocks no longer fall through to the real command. - If you call
Invoke-Pesterwith v4-style params (-Script,-OutputFile, …), switch toNew-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
-ForEachdata 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 ownBeforeAll. For setup every file needs, useRun.BeforeContainer(config) or aPester.BeforeContainer.ps1at 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:
You can disable the check for the whole run with
Describe 'Optional cases' -ForEach $cases -AllowNullOrEmptyForEach { It 'runs only when there is data' { } }Run.FailOnNullOrEmptyForEach = $false, but that brings back the silent skipping it's meant to catch — prefer fixing the data or using-AllowNullOrEmptyForEachwhere 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
-Inconclusiveor-Skipped, or mark the test withIt … -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
OutputFormatto'CoverageGutters'throws an invalid-value error. - Fix: use
JaCoCo(default) orCobertura.
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-Pesterparameters →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
TestResultorCodeCoveragesection auto-enables that section, so you no longer have to set.Enabled = $trueseparately 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 -Be → Should-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/-TestCasesthat can be empty marked-AllowNullOrEmptyForEach(or data fixed).- Duplicate
Before*/After*blocks combined. Assert-MockCalled→Should -Invoke;Assert-VerifiableMock→Should -InvokeVerifiable.- Default
Mockadded 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-Pesterlegacy params →New-PesterConfiguration.- Coverage
OutputFormatisJaCoCo/Cobertura;UseBreakpointsset only if old numbers needed. - Suite green on v6; diff reviewed; committed.