Upgrade pester v4 to v5
Skill PlagueHO/plagueho.copilot/skills/upgrade-pester-v4-to-v5
**WORKFLOW SKILL** — Migrate PowerShell Pester v4 tests to Pester v5, covering structural, assertion, and mock changes. WHEN: "upgrade Pester to v5", "migrate Pester v4 to v5", "convert Pester tests to Pester 5", "modernize Pester tests", "Assert-MockCalled to Should -Invoke". INVOKES: run_in_terminal for detection scripts and Invoke-Pester. FOR SINGLE OPERATIONS: edit one assertion or Invoke-Pester call directly.From its SKILL.md
npx -y skills add PlagueHO/plagueho.copilot --skill upgrade-pester-v4-to-v5Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.2 KB, as published. Nobody here has run it
Upgrade Pester v4 to v5
Migrate PowerShell test suites from Pester v4 to Pester v5 so they pass under the v5 two-phase (Discovery + Run) engine and conform to current conventions.
Required references — consult before and during migration:
The full breaking-change mapping lives in references/v4-to-v5-mapping.md. The target style rules live in references/pester5-conventions.md. Before/after examples are in assets/.
Why reliability matters here
A Pester v4→v5 migration silently changes behavior if done carelessly: code that
used to run inside Describe/Context now runs during Discovery, mocks are
scoped by placement rather than the whole block, and Should -Throw matches
with -like instead of .Contains. A test that still passes may no longer
test what it claims. Therefore this workflow is evidence-driven: establish a
green baseline first, migrate in small reviewable units, and re-run the suite
after every file. Never declare success without a passing Invoke-Pester run.
Prerequisites
- PowerShell 5.1+ or PowerShell 7+ (
pwshrecommended). - Pester 5.x installed:
Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck. - A clean source-control state so every change is reviewable and reversible.
- The module or scripts under test must be importable from the test files.
Process
Step 1 — Establish a baseline
Capture the current state so regressions are detectable:
- Confirm the working tree is committed (or stash unrelated changes).
- Record the installed Pester version:
Get-Module Pester -ListAvailable | Select-Object Version. - If Pester 4 is still available, run the suite once and save the result count.
This is the behavior you must preserve. If only Pester 5 is installed, capture
the current (likely failing)
Invoke-Pesteroutput instead — it becomes the error list to drive the migration.
Do not change test intent during migration. The goal is identical coverage under the new engine, not new or "improved" tests.
Step 2 — Detect v4 patterns
Run the detection script to inventory every file and the v4 constructs it contains. This produces the worklist; do not migrate blind.
PowerShell:
& "<skill-path>/scripts/Find-PesterV4Pattern.ps1" -Path . -OutputFormat Table
Shell:
"<skill-path>/scripts/find-pester-v4-pattern.sh" --path .
The script flags: legacy Should (no dash), Assert-MockCalled,
Assert-VerifiableMock(s), InModuleScope wrappers, $MyInvocation.MyCommand.Path,
top-level/Describe-body code, and deprecated Invoke-Pester parameters
(-Script, -TestName, -Show, -PesterOption, -Strict). Treat its output as
the checklist for Steps 3–5.
Step 3 — Migrate test-file structure (the high-risk change)
For each *.Tests.ps1 file, apply these structural rules. They address the
two-phase engine and are the most common cause of silent breakage:
-
Move all executable code into Pester blocks. No code may sit at the top of the file or directly inside a
Describe/Contextbody. Wrap shared setup inBeforeAll(runs once per block during Run), per-test setup inBeforeEach, and teardown inAfterAll/AfterEach. Teardown runs inside afinally, so it executes even when a test or its setup fails. Variables fromBeforeAllare visible to child blocks but read-only there (tests stay isolated);BeforeEach,It, andAfterEachshare one scope, soAfterEachcan see variables anItcreated. -
Import the code under test inside
BeforeAllusing$PSScriptRoot:BeforeAll { . $PSScriptRoot/Get-Thing.ps1 # or dot-source the sibling script: . $PSCommandPath.Replace('.Tests.ps1', '.ps1') # or import a module: Import-Module $PSScriptRoot/MyModule.psd1 -Force }Replace any
$MyInvocation.MyCommand.Pathpath discovery with$PSScriptRootor$PSCommandPath; the former does not work in v5BeforeAll. -
Move discovery-time code into
BeforeDiscovery. Code that builds-ForEach/-TestCasesdata, or computes-Skipconditions, runs during Discovery. Put it inBeforeDiscovery, notBeforeAll(which runs later). -
Pass data into tests explicitly. Variables created in Discovery are not visible in
It/BeforeAll. Supply them via-ForEach/-TestCasesso each test receives its own data. -
Do not wrap
Describe/ItinInModuleScope. To intercept a module's internal calls, preferMock -ModuleName MyModule(see Step 4). ReserveInModuleScopefor reaching non-exported functions, and place it inside anIt, never aroundDescribe/Context/It; wrapping blocks prevents testing exported functions, skips export validation, and slows Discovery by loading the module. Variables or functions created insideInModuleScopedo not persist by default; use thescript:scope modifier when they must outlive the scriptblock.
See references/v4-to-v5-mapping.md for the exhaustive rule table and assets/example-before.Tests.ps1 versus assets/example-after.Tests.ps1.
Step 4 — Migrate assertions and mocks
Apply these token-level replacements (full list in the mapping reference):
| v4 construct | v5 replacement |
|---|---|
Should Be x (no dash) | Should -Be x |
Should Throw 'msg' | Should -Throw '*msg*' (matches with -like) |
Assert-MockCalled Foo -Times 1 | Should -Invoke Foo -Times 1 |
Assert-VerifiableMock(s) | Should -InvokeVerifiable |
Critical mock behavior changes:
- Mocks are scoped by placement, not the whole
Describe/Context. A mock inBeforeAllcovers the block and its children; a mock inside anItcovers only thatIt. Move mocks deliberately and re-check every-Times/-Exactlyafter re-scoping. - Mock counting also depends on placement.
Should -Invokedefaults toItscope when called fromIt,BeforeEach, orAfterEach, and to the containingDescribe/Contextscope when called fromBeforeAll/AfterAll. Override with-Scope It/-Scope Context/-Scope Describewhen the assertion sits in a different block than the calls it counts. - Mock commands called inside a module with
-ModuleName. A plainMockonly intercepts calls made from the test script. To mock a command invoked by the module under test, useMock -ModuleName MyModule CommandName { ... }and verify withShould -Invoke CommandName -ModuleName MyModule. Inside anInModuleScope MyModule { ... }block the-ModuleNameis implicit and omitted. -ParameterFilterno longer needs aparam()block; reference the mocked command's parameters (for example$Path) directly.- Read bound parameters with
$PesterBoundParameters(Pester 5.2+) inside a mock body; the mock hook overwrites$PSBoundParameters. Should -Throwmatches with-like. Wrap expected substrings in*wildcards, or the assertion requires an exact message match and fails.
Step 5 — Migrate Invoke-Pester calls and configuration
Update runners, CI scripts, and build tasks:
-
-Script→-Path(paths only — hashtables are not accepted). -
-TestName→-FullNameFilter. -
-Show→-Outputwith one ofNone,Normal,Detailed,Diagnostic. -
Remove
-PesterOptionand-Strict(unsupported). -
Prefer the advanced interface for anything non-trivial:
$config = New-PesterConfiguration $config.Run.Path = './tests' $config.Output.Verbosity = 'Detailed' $config.TestResult.Enabled = $true $config.TestResult.OutputFormat = 'NUnitXml' Invoke-Pester -Configuration $config
For CI that still consumes the v4 result shape, convert with
ConvertTo-Pester4Result, or emit NUnit output via -CI / ConvertTo-NUnitReport.
The legacy-parameter mapping (EnableExit, CodeCoverage, OutputFile, …) is in
the mapping reference.
Step 6 — Validate after every file
Reliability gate: run the suite and require a green (or baseline-equivalent) result before moving on.
PowerShell:
& "<skill-path>/scripts/Invoke-PesterValidation.ps1" -Path . -Verbosity Detailed
Shell:
"<skill-path>/scripts/invoke-pester-validation.sh" --path .
The script runs Invoke-Pester with the advanced configuration, fails on any
errored/failed test, and surfaces Discovery errors (the signal that structural
migration in Step 3 is incomplete). If counts dropped versus the Step 1 baseline,
a test was silently skipped — investigate before continuing.
Step 7 — Apply conventions and finalize
Once green, align with references/pester5-conventions.md:
*.Tests.ps1naming; tests next to code or in a dedicated test directory.- Descriptive
Describe/Context/Itnames; AAA structure; isolated tests. - Full cmdlet names (no aliases); tags for filtering;
Set-ItResultfor runtime skips. - Add a
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.5.0' }guard (or update an existing v4 requirement) where appropriate.
Then run the full suite one final time and summarize: files changed, constructs replaced, and the before/after test counts.
Reliability checklist
Verify all of the following before declaring the migration complete:
- Baseline test count captured before any change (Step 1)
- Detection script run; every flagged construct addressed (Step 2)
- No executable code at file top level or directly in
Describe/Context - Code under test imported in
BeforeAllvia$PSScriptRoot - Discovery-time code moved to
BeforeDiscovery; data passed via-ForEach - All
Shouldassertions use the dashed form;Should -Throwuses*wildcards -
Assert-MockCalled→Should -Invoke;Assert-VerifiableMock(s)→Should -InvokeVerifiable - Mock placement and
-Times/-Exactlycounts re-checked after re-scoping - Commands called inside a module mocked with
-ModuleName;Should -Invokescope verified -
InModuleScope(if used) kept insideIt; noDescribe/Context/Itwrapped in it -
Invoke-Pesterparameters and CI scripts migrated to v5 / configuration object -
Invoke-Pesterpasses with no Discovery errors; counts match the baseline - Conventions applied;
#RequiresPester version updated to 5.x
Bundled assets
| Path | Purpose |
|---|---|
| scripts/Find-PesterV4Pattern.ps1 / .sh | Scan *.Tests.ps1 for v4 constructs and report line numbers |
| scripts/Invoke-PesterValidation.ps1 / .sh | Run Pester 5 and fail on failures or Discovery errors |
| references/v4-to-v5-mapping.md | Exhaustive v4→v5 breaking-change mapping |
| references/pester5-conventions.md | Target Pester 5 conventions and structure |
| assets/example-before.Tests.ps1 | Representative v4 test file |
| assets/example-after.Tests.ps1 | The same file migrated to v5 |