Installation

Install with CLI Recommended
gh skills-hub install pester-migration

Don't have the extension? Run gh extension install samueltauil/skills-hub first.

Download and extract to your repository:

.github/skills/pester-migration/

Extract the ZIP to .github/skills/ in your repo. The folder name must match pester-migration for Copilot to auto-discover it.

Skill Files (4)

SKILL.md 8.5 KB
---
name: pester-migration
description: 'Experimental (preview) Pester migration skill for upgrading PowerShell Pester test suites across major versions — v3→v4, v4→v5, and v5→v6. The v5→v6 path tracks Pester 6, which is still a release candidate, so that guidance may change. Covers the Discovery/Run two-phase model, moving setup into BeforeAll, $PSScriptRoot vs $MyInvocation, mock changes (Assert-MockCalled → Should -Invoke, removed fall-through), Invoke-Pester parameters → PesterConfiguration, data-driven -ForEach/-TestCases, and the v6 breaking changes. Use when the user asks to upgrade, migrate, or modernize Pester tests, fix *.Tests.ps1 files that broke after bumping the Pester version, or convert legacy Should / Invoke-Pester syntax.'
---

# Pester Migration

> **Experimental / preview.** The **v5→v6** guidance tracks Pester 6 while it is a release
> candidate and may change; verify against the current
> [release notes](https://github.com/pester/Pester/releases). v3→v4 and v4→v5 cover stable releases.

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](references/v3-to-v4.md) | `Should Be` → `Should -Be`, `Contain` → `FileContentMatch`, `Assert-VerifiableMocks` → `Assert-VerifiableMock`, array-assertion edge cases. |
| [v4-to-v5.md](references/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](references/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.

```powershell
# 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).Version
```

Tell 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](references/v3-to-v4.md) |
| `$MyInvocation.MyCommand.Path` + dot-source at the **top** of the file; arbitrary code directly under `Describe` | v4 → [v4-to-v5](references/v4-to-v5.md) |
| `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](references/v5-to-v6.md) |

Install the target version when ready:

```powershell
# Latest stable v5 — pin the major so this keeps installing v5 even after v6 goes GA
Install-Module Pester -MaximumVersion 5.99.99 -Force

# Pester 6 (currently a release candidate — needs -AllowPrerelease)
Install-Module Pester -AllowPrerelease -Force
```

> On **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 `-SkipPublisherCheck` there 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.

1. **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.
   ```powershell
   # Bare Invoke-Pester works on every major; exact parameters differ
   # (v3/v4: -Script/-OutputFile; v5+/v6: -Path/-Output).
   Invoke-Pester
   ```
2. **Read the reference** for this jump (table above) so you know the full scope before editing.
3. **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.
4. **Switch versions** with `Install-Module` (Step 0), then re-import: `Remove-Module Pester;
   Import-Module Pester` (or start a fresh session).
5. **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.
6. **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)
```powershell
# 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](references/v4-to-v5.md).

### v5 → v6 (most common fixes)
```powershell
# 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 one
```
Full breaking-change list with symptoms and fixes: [references/v5-to-v6.md](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 `Should` syntax 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 bisect` useful if a
  migrated test goes red later.
references/
v3-to-v4.md 3.6 KB
# Migrating Pester v3 → v4

This is the smallest jump — mostly an assertion-syntax rename. Many suites need only minor changes,
some need none. It is largely script-automatable, but always review the diff and re-run the suite.

Official guide: https://pester.dev/docs/migrations/v3-to-v4

> Heads-up: if your goal is a modern Pester (5 or 6), v3→v4 is only the first step. Do it, get
> green, then continue with [v4-to-v5.md](v4-to-v5.md) and [v5-to-v6.md](v5-to-v6.md).

---

## Change 1 — Dashed `Should` assertion syntax

v4 introduced the parameter-style `Should` syntax. The bareword form still ran in v4 but is
**removed in v5**, so converting now saves a later migration.

```powershell
# v3 (bareword)
It 'checks something' { 10 | Should Be 10 }

# v4+ (dashed)
It 'checks something' { 10 | Should -Be 10 }
```

The rename applies to every operator: `Be`, `BeExactly`, `Match`, `Throw`, `BeNullOrEmpty`,
`Contain`, etc. → `-Be`, `-BeExactly`, `-Match`, `-Throw`, `-BeNullOrEmpty`, …

There is a well-known AST-based converter, `Update-PesterTest` (Chris Dent / Wojciech Sciesinski),
that inserts the dashes safely by parsing the file rather than regexing it:
https://gist.github.com/indented-automation/aeb14825e39dd8849beee44f681fbab3 — it's also reproduced
in the official v3→v4 guide. Review its output, especially for non-UTF-8/ASCII files, where it can
change encoding.

---

## Change 2 — `Contain` → `FileContentMatch`

The `Contain` assertion was renamed to `FileContentMatch` (it tests file **contents**, which the
old name made ambiguous against collection containment).

```powershell
# Should Contain      -> Should -FileContentMatch
# Should Not Contain  -> Should -Not -FileContentMatch
'app.config' | Should -FileContentMatch 'setting'
'app.config' | Should -Not -FileContentMatch 'secret'
```

A simple regex-based migration script from the official guide (verify results — it can produce
false positives):

```powershell
$content = Get-Content -Path $file -Encoding $encoding
$content = $content -replace 'Should\s+\-?Contain',        'Should -FileContentMatch'
$content = $content -replace 'Should\s+\-?Not\s*-?Contain', 'Should -Not -FileContentMatch'
$content = $content -replace 'Assert-VerifiableMocks',      'Assert-VerifiableMock'
$content | Set-Content -Path $file -Encoding $encoding
```

---

## Change 3 — `Assert-VerifiableMocks` → `Assert-VerifiableMock`

The cmdlet was renamed (dropped the trailing `s`). Rename all occurrences.

> In Pester 5 this is *deprecated* and in Pester 6 it is *removed* — when you continue past v4,
> switch to `Should -InvokeVerifiable`. See [v5-to-v6.md](v5-to-v6.md).

---

## Change 4 — Array assertions (watch for edge cases)

`Should` gained array assertions in v4. This is transparent for most tests, but there are edge
cases where an array test that passed under v3 fails under v4. If an array-related test changes
result after the rename, inspect it manually rather than forcing it to pass. Background:
https://github.com/pester/Pester/issues/873.

Mocking also shifted subtly when Pester moved from functions to aliases; there are no required
changes, but if mocked-command behavior looks off, see
https://github.com/pester/Pester/issues/810 and https://github.com/pester/Pester/issues/812.

---

## v3 → v4 checklist

- [ ] Suite runs on v3 first (baseline).
- [ ] All `Should <Operator>` converted to `Should -<Operator>` (prefer the AST converter).
- [ ] `Should Contain` → `Should -FileContentMatch` (and the `-Not` form).
- [ ] `Assert-VerifiableMocks` → `Assert-VerifiableMock`.
- [ ] Array-assertion behavior changes reviewed manually.
- [ ] File encoding preserved by any scripted replacement.
- [ ] Suite green on v4; diff reviewed; committed.
v4-to-v5.md 11.0 KB
# 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.
v5-to-v6.md 10.5 KB
# 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.

> Pester 6 is currently a **release candidate**. Install with
> `Install-Module Pester -AllowPrerelease -Force` (add `-SkipPublisherCheck` on Windows
> PowerShell 5.1). Some details may still change before final — check the release notes:
> https://github.com/pester/Pester/releases.

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:

1. Run on **Windows PowerShell 5.1** or **PowerShell 7.4+** (older PS is dropped).
2. Check any `-ForEach`/`-TestCases` that can be empty — it now **throws** unless you add
   `-AllowNullOrEmptyForEach`.
3. Remove duplicate `BeforeAll`/`BeforeEach`/`AfterAll`/`AfterEach` in the same block.
4. Replace `Assert-MockCalled` / `Assert-VerifiableMock` with `Should -Invoke` /
   `Should -InvokeVerifiable`.
5. Add a default `Mock` where some calls don't match a `-ParameterFilter` — mocks no longer fall
   through to the real command.
6. If you call `Invoke-Pester` with v4-style params (`-Script`, `-OutputFile`, …), switch to
   `New-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
  `-ForEach` data 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 own `BeforeAll`. For setup every file needs, use
  `Run.BeforeContainer` (config) or a `Pester.BeforeContainer.ps1` at the repo root.

```powershell
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:
  ```powershell
  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
  `-AllowNullOrEmptyForEach` where 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:
  ```powershell
  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.
  ```powershell
  # 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:
  ```powershell
  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:**
  ```powershell
  # 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:
  ```powershell
  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 `-Inconclusive` or `-Skipped`, or mark the test with `It … -Skip`:
  ```powershell
  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 `OutputFormat` to `'CoverageGutters'` throws an invalid-value error.
- **Fix:** use `JaCoCo` (default) or `Cobertura`.

### `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-Pester` parameters → `New-PesterConfiguration`" section of [v4-to-v5.md](v4-to-v5.md)):
  ```powershell
  $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 `TestResult` or
> `CodeCoverage` section auto-enables that section, so you no longer have to set `.Enabled = $true`
> separately 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`/`-TestCases` that can be empty marked `-AllowNullOrEmptyForEach` (or data fixed).
- [ ] Duplicate `Before*`/`After*` blocks combined.
- [ ] `Assert-MockCalled` → `Should -Invoke`; `Assert-VerifiableMock` → `Should -InvokeVerifiable`.
- [ ] Default `Mock` added 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-Pester` legacy params → `New-PesterConfiguration`.
- [ ] Coverage `OutputFormat` is `JaCoCo`/`Cobertura`; `UseBreakpoints` set only if old numbers needed.
- [ ] Suite green on v6; diff reviewed; committed.

License (MIT)

View full license text
MIT License

Copyright GitHub, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.