Files
awesome-copilot/skills/pester-migration/references/v4-to-v5.md
T
github-actions[bot] c53d11e7a0 chore: publish from main
2026-07-02 00:30:07 +00:00

283 lines
11 KiB
Markdown

# 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.ps1` file top to bottom but only to *find* tests.
It invokes the `Describe`/`Context` script blocks to collect the tree of `It`s, evaluates `It`
`-Name` strings and `-TestCases`/`-ForEach` data, and records `BeforeAll`/`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:**
1. Put **all** test code inside `It`, `BeforeAll`, `BeforeEach`, `AfterAll`, or `AfterEach`.
2. Put **no** test code directly in `Describe`/`Context` bodies or at the top of the file —
unless it is meant to build tests, in which case it goes in `BeforeDiscovery`.
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.
```powershell
# 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 '🌵' }
}
```
```powershell
# 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.Path` is 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`.
```powershell
# 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):
```powershell
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.
```powershell
# 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`:
```powershell
$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.
```powershell
# 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 the `Mock` in the
same `BeforeAll`/`It` scope where it applies, and assert counts in that scope.
- **`Assert-VerifiableMocks` was removed.** Use `Should -InvokeVerifiable`. (`Assert-MockCalled`
and `Assert-VerifiableMock` still exist but are *deprecated* in v5 and **removed in v6** — prefer
`Should -Invoke` / `Should -InvokeVerifiable` now to save a second migration. See
[v5-to-v6.md](v5-to-v6.md).)
- **Mocks are debuggable.** v5 no longer rewrites your mock script block, so you can set
breakpoints inside `-MockWith` and inside `-ParameterFilter`.
- **Avoid `InModuleScope` around `Describe`/`It`.** It loads the module during Discovery (slowing
it down) and lets you test internals instead of the published surface. Prefer `-ModuleName` on
`Mock` and on `Should -Invoke`; if you must use `InModuleScope`, keep it inside `It`. See
https://pester.dev/docs/usage/mocking.
```powershell
# Prefer this over wrapping the whole Describe in InModuleScope
Mock Get-Internal -ModuleName MyModule { 'mocked' }
Should -Invoke Get-Internal -ModuleName MyModule -Times 1 -Exactly
```
---
## Fix 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`.
```powershell
# 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 to `Should -Be` ([v3-to-v4.md](v3-to-v4.md)).
- **Gherkin** was removed — stay on Pester v4 if you need it.
- `-Output`/`-Show` reduced to `None`, `Normal`, `Detailed`, `Diagnostic`.
- `-TestName``-FullNameFilter`; `-Script``-Path` (paths only); `-PesterOption` removed.
---
## v4 → v5 checklist
- [ ] Suite runs green on v4 first (baseline).
- [ ] File import moved into `BeforeAll`; `$MyInvocation.MyCommand.Path` replaced with
`$PSScriptRoot`/`$PSCommandPath`.
- [ ] No loose code in `Describe`/`Context` bodies or at file top-level; test-generating code moved
to `BeforeDiscovery`.
- [ ] `foreach`-generated tests converted to `-ForEach`; per-item data passed via `-ForEach`/`-TestCases`.
- [ ] `-Skip:` conditions don't depend on `BeforeAll` variables.
- [ ] `Should -Throw` messages use `-like` wildcards (`*...*`).
- [ ] Mocks defined in the right scope; `Assert-VerifiableMocks``Should -InvokeVerifiable`;
`InModuleScope` removed from around `Describe`/`It` in favor of `-ModuleName`.
- [ ] `Invoke-Pester` call converted to Simple params or `New-PesterConfiguration`.
- [ ] Suite green on v5 with `-Output Detailed`; diff reviewed; committed.