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/v4-to-v5.md
Migrating Pester v4 → v5
This is the hard jump. v5 introduced a new runtime that splits a test run into two phases — Discovery and Run — and that changes how you must structure tests. It is not a pure find-and-replace. Read this whole file before editing a suite.
Official guide: https://pester.dev/docs/migrations/v4-to-v5 · Breaking changes: https://pester.dev/docs/migrations/breaking-changes-in-v5
The one concept that explains everything: Discovery and Run
A v5+ run happens in two passes:
Discovery — Pester executes each
*.Tests.ps1file top to bottom but only to find tests. It invokes theDescribe/Contextscript blocks to collect the tree ofIts, evaluatesIt-Namestrings and-TestCases/-ForEachdata, and recordsBeforeAll/It/etc. script blocks without running them.Run — Pester then executes the recorded setups, tests, and teardowns with correct scoping.
The two rules that make a suite v5-correct:
Put all test code inside
It,BeforeAll,BeforeEach,AfterAll, orAfterEach.Put no test code directly in
Describe/Contextbodies or at the top of the file — unless it is meant to build tests, in which case it goes inBeforeDiscovery.
Code that sits loose in a Describe body or at file top-level runs during Discovery, and its
results are usually not available during Run. This is the root cause of most "it worked in
v4, it's $null in v5" bugs.
Fix 1 — Move file setup into BeforeAll and use $PSScriptRoot
The classic v4 header dot-sources the system-under-test at file scope using
$MyInvocation.MyCommand.Path. Both the placement and that variable break in v5.
# BEFORE (v4)
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace('.Tests.', '.')
. "$here\$sut"
Describe 'Get-Cactus' {
It 'Returns 🌵' { Get-Cactus | Should -Be '🌵' }
}# AFTER (v5+)
BeforeAll {
# Do NOT use $MyInvocation.MyCommand.Path here.
. $PSScriptRoot/Get-Cactus.ps1
# or, by convention from the test file name:
# . $PSCommandPath.Replace('.Tests.ps1', '.ps1')
}
Describe 'Get-Cactus' {
It 'Returns 🌵' { Get-Cactus | Should -Be '🌵' }
}Why $MyInvocation.MyCommand.Path fails: it returns the path only when evaluated directly in the
script body. Inside any function or script block (and BeforeAll is a script block) Path is
empty. Use $PSScriptRoot (the test file's directory) or $PSCommandPath (the test file's full
path) instead. string.Replace('.Tests.ps1','.ps1') is case-sensitive — keep the .Tests.ps1
casing exact.
$MyInvocation.MyCommand.Pathis fine inside your module/product code — the change only affects the test-file header pattern. See https://pester.dev/docs/usage/importing-tested-functions#migrating-from-pester-v4.
There is a community migration script that does the BeforeAll wrap for you (review its output): https://gist.github.com/nohwnd/d488bd14ab4572f92ae77e208f476ada
Fix 2 — Generate tests with BeforeDiscovery + -ForEach, not loose foreach
A very common v4 pattern builds tests from data with a top-level foreach. In v5 the data often
isn't defined at Discovery time, so no tests get generated, or the per-item variable is missing
inside It.
# BROKEN in v5: $files is set in BeforeAll (Run phase), but the foreach runs in Discovery
BeforeAll { $files = Get-ChildItem *.ps1 }
foreach ($file in $files) {
Describe "$file is correct" {
It 'has empty line at end' { }
}
}Two things must change: build the data in BeforeDiscovery (so it exists during Discovery), and
pass per-item data into the test with -ForEach/-TestCases (so It can see it during Run):
BeforeDiscovery {
$files = Get-ChildItem *.ps1 # runs during Discovery
}
Describe 'script <_> is correct' -ForEach $files {
It 'has an empty line at the end' {
# $_ is the current file here
}
}Prefer -ForEach on the block/It over a hand-written foreach; it both creates the copies and
makes the current item available. Use <_> (or <Name> for hashtable items) in the name to
template per-item titles. Reference: https://pester.dev/docs/usage/data-driven-tests.
Fix 3 — -Skip and -TestCases are evaluated during Discovery
Because filters and data are resolved during Discovery, conditions computed in BeforeAll are not
available yet.
# DOES NOT skip: $isSkipped is set in BeforeAll (Run), but -Skip is read in Discovery
Describe 'd' {
BeforeAll { $isSkipped = Get-IsSkipped }
It 'i' -Skip:$isSkipped { }
}Move cheap skip logic to file scope (it runs on every Discovery) or, better, base it on a static
global like $IsWindows:
$isSkipped = -not $IsWindows
Describe 'd' {
It 'i' -Skip:$isSkipped { }
}Keep Discovery-time code cheap — it runs every time the file is discovered, which can be often.
Fix 4 — Variables don't leak from Discovery into the test
Variables defined during Discovery are not visible in BeforeAll/-Each, AfterAll/-Each, or
It. If you compute something while generating tests and need it at run time, attach it to the
test via -ForEach/-TestCases. (TestDrive is Run-only and likewise can't be used in
-ForEach.)
Fix 5 — Should -Throw matches with -like wildcards
In v5, Should -Throw <message> matches the exception message with -like, not .Contains(). A
substring that used to match now needs wildcards.
# v4: matched a substring
{ throw 'connection failed: timeout' } | Should -Throw 'timeout'
# v5+: use wildcards to match part of the message
{ throw 'connection failed: timeout' } | Should -Throw '*timeout*'Fix 6 — Mocks: scoping, debugging, and InModuleScope
Scope follows placement. In v5, mocks (and their call counts) are scoped to where you put them — the current block/test — not the entire
Describe/Context. Define theMockin the sameBeforeAll/Itscope where it applies, and assert counts in that scope.Assert-VerifiableMockswas removed. UseShould -InvokeVerifiable. (Assert-MockCalledandAssert-VerifiableMockstill exist but are deprecated in v5 and removed in v6 — preferShould -Invoke/Should -InvokeVerifiablenow to save a second migration. See v5-to-v6.md.)Mocks are debuggable. v5 no longer rewrites your mock script block, so you can set breakpoints inside
-MockWithand inside-ParameterFilter.Avoid
InModuleScopearoundDescribe/It. It loads the module during Discovery (slowing it down) and lets you test internals instead of the published surface. Prefer-ModuleNameonMockand onShould -Invoke; if you must useInModuleScope, keep it insideIt. See https://pester.dev/docs/usage/mocking.
# Prefer this over wrapping the whole Describe in InModuleScope
Mock Get-Internal -ModuleName MyModule { 'mocked' }
Should -Invoke Get-Internal -ModuleName MyModule -Times 1 -ExactlyFix 7 — Invoke-Pester parameters → New-PesterConfiguration
Invoke-Pester's interface was overhauled. v5 kept a deprecated compatibility set so v4 calls
mostly still run (with a warning), but you should move to either the Simple parameters or the
Advanced -Configuration object. (v6 removes the legacy set entirely — migrate now.)
Simple interface (parameter → config property):
| Simple parameter | Configuration property |
|---|---|
-Path | Run.Path |
-ExcludePath | Run.ExcludePath |
-Tag | Filter.Tag |
-ExcludeTag | Filter.ExcludeTag |
-FullNameFilter | Filter.FullName |
-Output | Output.Verbosity |
-CI | TestResult.Enabled + Run.Exit (both $true) |
-PassThru | Run.PassThru |
Legacy (v4) parameters → config:
| v4 parameter | Configuration property |
|---|---|
-Script | Run.Path (paths only — no hashtables) |
-EnableExit | Run.Exit |
-TestName | replaced by -FullNameFilter / Filter.FullName |
-CodeCoverage | CodeCoverage.Path (+ CodeCoverage.Enabled = $true) |
-CodeCoverageOutputFile | CodeCoverage.OutputPath |
-CodeCoverageOutputFileEncoding | CodeCoverage.OutputEncoding |
-CodeCoverageOutputFileFormat | CodeCoverage.OutputFormat |
-OutputFile | TestResult.OutputPath (+ TestResult.Enabled = $true) |
-OutputFormat | TestResult.OutputFormat |
-Show / -Output | Output.Verbosity (see mapping below) |
-PesterOption, -Strict | ignored / not available |
-Show value → Output.Verbosity: All/Default/Detailed → Detailed; Fails/Normal →
Normal; Diagnostic → Diagnostic; Minimal → Minimal; None → None.
# BEFORE (v4 legacy)
Invoke-Pester -Script ./tests -CodeCoverage ./src/*.ps1 `
-OutputFile result.xml -OutputFormat NUnitXml -EnableExit
# AFTER (v5 Advanced)
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Exit = $true
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './src'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'result.xml'
$config.TestResult.OutputFormat = 'NUnitXml'
Invoke-Pester -Configuration $config-Output Diagnostic is your best friend while migrating — it shows Discovery/Skip/Mock decisions.
The new result object
The v5 result object is much richer and is what Pester uses internally. To keep a v4-era CI
pipeline working, convert it with ConvertTo-Pester4Result. For NUnit output use
ConvertTo-NUnitReport, or pass -CI to enable NUnit output, code coverage, and a failing exit
code in one switch. Each test's -TestCases/-ForEach item is available on the test object's
Data property.
Other removed / changed things in v5
PowerShell 2 is no longer supported.
Legacy
Should Be(no dash) is removed — convert toShould -Be(v3-to-v4.md).Gherkin was removed — stay on Pester v4 if you need it.
-Output/-Showreduced toNone,Normal,Detailed,Diagnostic.-TestName→-FullNameFilter;-Script→-Path(paths only);-PesterOptionremoved.
v4 → v5 checklist
[ ] Suite runs green on v4 first (baseline).
[ ] File import moved into
BeforeAll;$MyInvocation.MyCommand.Pathreplaced with$PSScriptRoot/$PSCommandPath.[ ] No loose code in
Describe/Contextbodies or at file top-level; test-generating code moved toBeforeDiscovery.[ ]
foreach-generated tests converted to-ForEach; per-item data passed via-ForEach/-TestCases.[ ]
-Skip:conditions don't depend onBeforeAllvariables.[ ]
Should -Throwmessages use-likewildcards (*...*).[ ] Mocks defined in the right scope;
Assert-VerifiableMocks→Should -InvokeVerifiable;InModuleScoperemoved from aroundDescribe/Itin favor of-ModuleName.[ ]
Invoke-Pestercall converted to Simple params orNew-PesterConfiguration.[ ] Suite green on v5 with
-Output Detailed; 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