Migrating from Pester 5 to 6
Learn how to easily migrate your test suite from v5 to v6
Recently, the Pester repository has seen a good amount of pull requests by the core maintainer. And after those PRs, 4 release candidates were created.
Whilst there isn't any official GA for v6 yet, the release candidates already give a glimpse of what is changing. Thus, in this article, we'll be looking at the already available release candidates and looking at how you can migrate from v5 to v6.
Don't worry if your test suite is on Pester 5. The core shapes still look familiar with the famous syntaxes like Describe, Context, It, and others.
What changed in Pester 6?
Pester 6 still builds on top of Pester's 5 runtime, but it focuses on three key improvement areas:
- A new family of
Should-*assertions. - Faster code coverage, using profiler-based coverage by default.
- An experimental parallel runner that runs test files concurrently.
The good news is that classic Pester 5 assertions still work. You don't need to convert every Should -Be into Should-Be on day one. The new assertions look like the following (which makes migration much less risky):
# Pester 5 style: still works in Pester 6
$result | Should -Be 'ok'
# Pester 6 style: new assertion command
$result | Should-Be 'ok'This new assertion family gives you an easy compatibility migration first, as you can modernize it over time. Or you can create a copilot-instructions.md file to help you out if you're using AI.
[!NOTE]
Want to see the experimental parallel runner in action? I created a dedicated post on this link.
Runtime support changed
Still using older versions of PowerShell? Bad luck. Pester 6 supports Windows PowerShell 5.1 and PowerShell 7.4+.
That makes the runtime version the first compatibility boundary to know about. Your local shell and CI agents need to be on a supported version:
$PSVersionTable.PSVersionMock verification moved to Should-Invoke
Pester 6 removes Assert-MockCalled and Assert-VerifiableMock. Use Should -Invoke, Should -InvokeVerifiable, or the new command-style assertions.
| Pester 5 | Pester 6 with classic Should syntax |
Pester 6 new style |
|---|---|---|
Assert-MockCalled Get-Thing |
Should -Invoke Get-Thing |
Should-Invoke Get-Thing |
Assert-MockCalled Get-Thing -Times 1 -Exactly |
Should -Invoke Get-Thing -Times 1 -Exactly |
Should-Invoke Get-Thing -Times 1 -Exactly |
Assert-MockCalled Get-Thing -ParameterFilter { $Id -eq 1 } |
Should -Invoke Get-Thing -ParameterFilter { $Id -eq 1 } |
Should-Invoke Get-Thing -ParameterFilter { $Id -eq 1 } |
Assert-VerifiableMock |
Should -InvokeVerifiable |
Should-Invoke -Verifiable |
Pester 6 also improves failed mock assertion output. When `Should-Invoke` fails, it can print mock invocation history and show which calls matched the parameter filter.

Dedidcated Should-* assertion commands
The introduction already mentioned it, but the headline feature in Pester 6 is the new Should-* assertion family. The difference is literally tiny but important to know:
# Pester 5 style
$value | Should -Be 42
# Pester 6 style
$value | Should-Be 42Its change mainly sits within the codebase. The old Should command had many operators behind one entry point. The new commands are specialized and type-aware, which gives Pester better failure messages. It also has a clearer behavior to deal with strings, arrays, $null , and typed values.
Here's a full list of how common Pester 5 assertions map to the new commands:
| Pester 5 assertion | Pester 6 assertion | Notes |
|---|---|---|
Should -Be |
Should-Be |
Generic value equality, similar to PowerShell -eq. |
Should -Not -Be |
Should-NotBe |
Generic inequality. |
Should -BeExactly |
Should-BeString -CaseSensitive |
Prefer this for exact string comparison. |
Should -Match |
Should-MatchString |
Regex string match. |
Should -BeLike |
Should-BeLikeString |
Wildcard string match. |
Should -BeTrue |
Should-BeTrue or Should-BeTruthy |
Choose strict boolean vs. truthy behavior deliberately. |
Should -BeFalse |
Should-BeFalse or Should-BeFalsy |
Choose strict boolean vs. falsy behavior deliberately. |
Should -BeNullOrEmpty |
Should-BeNull or a collection assertion |
Be explicit about whether you expect $null or an empty collection. |
Should -HaveCount |
Should-BeCollection -Count |
Use collection assertions for collection shape. |
Should -Contain |
Should-ContainCollection |
Checks collection contents. |
Should -Throw |
Should-Throw |
New command-style assertion. |
Should -HaveParameter |
Should-HaveParameter |
Command metadata assertion. |
Should -BeOfType / Should -HaveType |
Should-HaveType |
Type assertion. |
| Deep object comparison patterns | Should-BeEquivalent |
Best for rich objects, hashtables, API responses, and nested data. |
Pipeline input and -Actual are more explicit
PowerShell unwraps pipeline input. That means a typed array or single-item array can arrive differently than you expect.
# These are treated the same by value assertions
1 | Should-Be 1
@(1) | Should-Be 1If you want to be more specific about it, you can use collection-specific commands as such:
1, 2, 3 | Should-BeCollection @(1, 2, 3)
1, 2, 3 | Should-BeCollection -Count 3Now, if you want to take it one step further and want to preserve the exact object or collection type, use the -Actual:
Should-HaveType -Actual ([int[]](1, 2)) -Expected ([int[]])The following three bullet points are a small, simple mental model when you're writing a test:
- Use the pipeline for simple scalar values.
- Use collection assertions for arrays and lists.
- Use `-Actual` when the exact collection object or type matters.
Empty -Foreach data now fails discovery
Pester 6 now introduces an early failure when discovery on -Foreach receives a $null or an empty collection. This behavior is controlled by the Run.FailOnNullOrEmptyForeach, which by default is on.
The following test will succeed, as the data is already fed during BeforeDiscovery phase:
BeforeDiscovery {
$users = @(
@{ Name = 'Gijs'; Role = 'Admin' }
# Other users
)
}
Describe 'users' -ForEach $users {
It '<Name> has a name' {
$_.Name | Should-NotBeNull
}
}Change the BeforeDiscovery to BeforeAll, and the test will fail:

You can already see from the above image that if you want to relax the behavior, you can change it as such:
BeforeDiscovery {
$users = Get-Users
}
Describe 'users' -ForEach $users -AllowNullOrEmptyForEach {
It '<Name> has a name' {
$_.Name | Should-NotBeNull
}
}Or when you want to have it turned off globally:
$config = New-PesterConfiguration
$config.Run.FailOnNullOrEmptyForEach = $false
Invoke-Pester -Configuration $configJust keep in mind that the local option is more precise when an empty data set is meaningful for a certain code block.
Discovery and run happen per file
This is probably the biggest "wait, why did my suite change" item in Pester 6.
Many suites can depend on discovery happening across the whole suite before anything actually runs. That's because many suites already set everything up. Sounds vague? Let's take a look.
In Pester 5, discovery happened globally: Pester discovered every file first, then ran tests.
Pester 6 does it differently. Discovery and run happen per file. Pester discovers one file, runs it, then moves to the next. This enables parallel execution, but it also means discovery-time side effects from one file should not be relied on by another file.
This can simply break suites that accidentally depend on the file order you might have:
# File A
Import-Module ./TestHelpers.psm1
# File B
Describe 'something' {
It 'uses helper from File A' {
Get-TestData | Should-NotBeNull
}
}The better durable shape is for each file to import what it needs:
BeforeAll {
Import-Module "$PSScriptRoot/TestHelpers.psm1" -Force
}But there's also a more global option if many files need the same bootstrapping:
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.BeforeContainer = {
. './tests/setup.ps1'
}
Invoke-Pester -Configuration $configIt's even possible to use a repository-level convention file, which is Pester.BeforeContainer.ps1.
Code coverage is profiler-based by default
Pester 6 uses profiler-based coverage. But what is it actually?
In Pester 5, coverage was collected by placing these breakpoints on commands. When breakpoints were hit, it got recorded. While this works, it can be expensive in larger codebases because the test run has to manage a lot of instrumentation.
Profiler-based coverage uses the same tracing mechanism PowerShell uses for profiling. Pester now observes command execution through that tracer and records which lines or commands were visited. The result should be much faster for large suites, while still producing the coverage reports you expect.
Now, this is mostly internal changes, but if you need the older breakpoint-based behavior, you can configure it explicitly by:
$config = New-PesterConfiguration
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.UseBreakpoints = $true
Invoke-Pester -Configuration $configPester 6 removes CoverageGutters, so you should use JaCoCo or Cobertura.
Disable Pester 5 assertion syntax
Is your suite adopted with the new assertion style? Perfect, you can know for sure by intentionally setting Should.DisableV5:
$config = New-PesterConfiguration
$config.Should.DisableV5 = $true
Invoke-Pester -Configuration $configWith this enabled, Should -Be throws. That makes it useful to know for certain.
The short version
Here's a short version if you already use Pester 5: most of the test authoring model carries forward, but these are the changes to look for.
- Classic
Should -Beassertions still work, but Pester 6 adds dedicatedShould-*commands. Assert-MockCalledandAssert-VerifiableMockare gone; mock verification now lives underShould -Invoke,Should -InvokeVerifiableandShould-Invoke.-ForEach $nulland-ForEach @()now fail discovery by default.- Discovery and run now happen per file, so test files need to carry their own discovery-time setup.
- Code coverage is profiler-based by default, and `CoverageGutters` output was removed.
- Pester 6 runs on Windows PowerShell 5.1 and PowerShell 7.4+.