Pester Migration
GitHubOfficialFirst committed by nohwndDistributed by GitHub
- Skill
- Workflow
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a systematic, version-by-version migration path for PowerShell Pester test suites. It addresses the fundamental architectural shifts between Pester versions, specifically the transition from v3 to v4 syntax, the complex v4 to v5 runtime changes (Discovery/Run phases), and the v5 to v6 breaking changes. The skill emphasizes a safe, incremental approach—migrating one major version at a time—to ensure test suites remain functional and reliable throughout the upgrade process.
What it does
- Provides step-by-step migration paths for v3 to v4, v4 to v5, and v5 to v6 transitions.
- Explains the Pester v5+ Discovery/Run two-phase runtime model.
- Maps legacy syntax and parameters to modern Pester equivalents.
- Guides the conversion of test setup logic into BeforeAll and BeforeDiscovery blocks.
- Offers specific remediation steps for common breaking changes like mock assertions and configuration objects.
- Includes safety rules for maintaining test integrity and using version control during migration.
When to use it
- When upgrading a legacy PowerShell test suite to a newer major version of Pester.
- When tests fail or behave unexpectedly after bumping the Pester module version.
- When modernizing test code to use PesterConfiguration instead of legacy Invoke-Pester parameters.
- When refactoring tests to comply with the v5+ Discovery/Run architecture.
Advantages
- Prevents common pitfalls by enforcing a one-major-version-at-a-time migration strategy.
- Reduces debugging time by providing symptom-driven fix tables for each version jump.
- Ensures test reliability by prioritizing structural correctness over simple find-and-replace.
- Helps maintain compatibility with modern PowerShell environments and Pester features.
Usage examples
- “How do I migrate my Pester v4 tests to v5?”
- The agent will guide you through the Discovery/Run phase split, explain how to move setup code into BeforeAll, and provide instructions for updating your test file headers to use $PSScriptRoot.
- “My tests are failing after updating to Pester 6, what changed?”
- The agent will identify that Assert-MockCalled has been removed and instruct you to replace it with Should -Invoke, while also checking for other v6-specific breaking changes.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/pester-migration/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip pester-migration.zip -d .agents/skills
Documentation by nohwnd
Pester Migration
Pester is the test framework for PowerShell. Test files end in *.Tests.ps1 and use
Describe / Context / It blocks with Should assertions. This skill upgrades an existing
suite from one major Pester version to the next and gets it green again.
Mental model: each major jump has a different character. v3→v4 is mostly a syntax rename. v4→v5 is a fundamental runtime change (the Discovery/Run split) and is the hard one. v5→v6 is largely backwards-compatible — a handful of previously-deprecated things now throw. Migrate one major at a time; never skip a version.
Detailed, symptom-driven guides live in references/ — load the one(s) for the jump you are doing.
References
| Reference | When to load |
|---|---|
| v3-to-v4.md | Should Be → Should -Be, Contain → FileContentMatch, Assert-VerifiableMocks → Assert-VerifiableMock, array-assertion edge cases. |
| v4-to-v5.md | The big one. Discovery/Run phases, BeforeAll setup, $PSScriptRoot, BeforeDiscovery, -ForEach, mock scoping, Should -Throw wildcards, Invoke-Pester → New-PesterConfiguration. |
| v5-to-v6.md | PowerShell 5.1/7.4+ only, per-file discovery+run, empty -ForEach throws, duplicate setup blocks throw, name <...> templates evaluate, Assert-MockCalled removed, mocks no longer fall through, code-coverage tracer, legacy Invoke-Pester params removed. |
Canonical source: the official migration guides at https://pester.dev/docs/migrations/ — this skill mirrors them. When in doubt, prefer the website.
Step 0 — Detect where you are and where you're going
Find the installed version(s) and the version the tests were written for. These can differ.
# Installed Pester version(s) on this machine
Get-Module Pester -ListAvailable | Select-Object Name, Version, Path
# Version currently imported in the session
(Get-Module Pester).VersionTell the source version from the test code with these heuristics:
You see in *.Tests.ps1 / build scripts | Suite was written for |
|---|---|
Should Be / Should Contain (no dash) | v3 or earlier → start at v3-to-v4 |
$MyInvocation.MyCommand.Path + dot-source at the top of the file; arbitrary code directly under Describe | v4 → v4-to-v5 |
Assert-MockCalled, Assert-VerifiableMock, Set-ItResult -Pending | v4 / early-v5 (these are removed in v6) |
Invoke-Pester -Script … -OutputFile … -CodeCoverage … (legacy params) | v4 invocation → map to config |
BeforeAll { . $PSScriptRoot/… }, New-PesterConfiguration, Should -Invoke | already v5-style → v5-to-v6 |
Install the target version when ready:
# Latest stable v5 — pin the major to avoid installing Pester 6
Install-Module Pester -MaximumVersion 5.99.99 -Force
# Pester 6
Install-Module Pester -ForceOn Windows PowerShell 5.1 the OS ships a Microsoft-signed built-in Pester 3 that PowerShellGet won't overwrite with the differently-signed newer Pester — add
-SkipPublisherCheckthere to install side-by-side. Not needed on PowerShell 7+. See https://pester.dev/docs/introduction/installation.
Migration workflow
Run this loop for each major jump. Do not jump two majors at once — go v4→v5, then v5→v6.
Baseline. Run the suite on the current version first and record pass/fail. You need a known-good (or known) starting point so you can tell migration regressions apart from pre-existing failures.
# Bare Invoke-Pester works on every major; exact parameters differ # (v3/v4: -Script/-OutputFile; v5+/v6: -Path/-Output). Invoke-PesterRead the reference for this jump (table above) so you know the full scope before editing.
Edit file by file. Apply the mechanical changes (see per-jump cheat sheets below and in the reference). Keep changes small and reviewable — one file or one concern at a time.
Switch versions with
Install-Module(Step 0), then re-import:Remove-Module Pester; Import-Module Pester(or start a fresh session).Run and fix. Re-run with
-Output Detailed; use-Output Diagnostic(v4→v5) or read the explicit v6 error messages to locate problems. Match each failure to the symptom → fix tables in the reference.Green, diff, commit. Re-run until the result matches the baseline (or better). Review the diff, then commit. Migrating in small commits makes regressions trivial to bisect.
What actually changes (scope per jump)
| Jump | Difficulty | Nature |
|---|---|---|
| v3 → v4 | Low | Assertion-syntax rename (Should -Be). Largely script-automatable. |
| v4 → v5 | High | New two-phase runtime. Test structure changes: setup must move into BeforeAll, discovery-time code into BeforeDiscovery, file location via $PSScriptRoot. Not a pure find-replace. |
| v5 → v6 | Low–Medium | Backwards-compatible runtime; deprecated features now throw. Mostly small, targeted fixes. Your Should -Be assertions keep working unchanged. |
Quick cheat sheets
v4 → v5 (most common fixes)
# 1. Move file import into BeforeAll, use $PSScriptRoot (NOT $MyInvocation.MyCommand.Path)
# BEFORE
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$here\Get-Thing.ps1"
# AFTER
BeforeAll { . $PSScriptRoot/Get-Thing.ps1 }
# 2. Any code that DISCOVERS/generates tests must be in BeforeDiscovery
BeforeDiscovery { $cases = Get-Content $PSScriptRoot/cases.json | ConvertFrom-Json }
# 3. Should -Throw matches with -like wildcards, not .Contains
{ throw 'a long message' } | Should -Throw '*long*'
# 4. Invoke-Pester legacy params → New-PesterConfiguration (see reference for full map)Full details, scoping rules, and the parameter→config table: references/v4-to-v5.md.
v5 → v6 (most common fixes)
# 1. Mock assertions: removed verbs — rename (old -> new):
# Assert-MockCalled -> Should -Invoke
# Assert-VerifiableMock -> Should -InvokeVerifiable
Should -Invoke Get-Thing -Times 1 -Exactly
Should -InvokeVerifiable
# 2. Add a default mock — unmatched calls no longer run the real command
Mock Get-Thing { 'default' }
Mock Get-Thing -ParameterFilter { $Name -eq 'a' } -MockWith { 'a' }
# 3. Empty/$null -ForEach now throws; allow it only where empty is expected
Describe 'Optional' -ForEach $cases -AllowNullOrEmptyForEach { }
# 4. Combine duplicate BeforeAll/BeforeEach/AfterAll/AfterEach in the same block into oneFull breaking-change list with symptoms and fixes: references/v5-to-v6.md.
Safety rules
Tests are the spec. Migration must not change what a test asserts — only how the suite is structured and invoked. If a test starts passing/failing differently for any reason other than a documented breaking change, investigate before accepting it.
Automated migration scripts produce false positives. The community scripts (linked in the references) help with
Shouldsyntax and dot-sourcing, but always review the diff and re-run the suite afterward. Never bulk-edit and commit unchecked.Mind file encoding when scripting replacements over
*.Tests.ps1— preserve the original encoding (UTF-8 vs ASCII) so you don't mangle non-ASCII test names.Work on a branch, commit per file/concern. Small commits keep
git bisectuseful if a migrated test goes red later.
Files
- ATTRIBUTION.mdAdded by AutoGineer861 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md8.1 KiB
- references/v3-to-v4.md3.7 KiB
- references/v4-to-v5.md11.1 KiB
- references/v5-to-v6.md10.3 KiB
references/v5-to-v6.md
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:
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-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.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
nohwnd
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/pester-migration · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- Gijsreyn, nohwnd
AutoGineer is not affiliated with or endorsed by nohwnd or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pester-migration.zip