Migrating from Pester 5 to 6

Learn how to easily migrate your test suite from v5 to v6

Migrating from Pester 5 to 6

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.PSVersion

Mock 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.

Figure 1: Improved assertion output

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 42

Its 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 1

If 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 3

Now, 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:

Figure 2: Test fail because collection is empty

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 $config

Just 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 $config

It'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 $config

Pester 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 $config

With 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.

  1. Classic Should -Be assertions still work, but Pester 6 adds dedicated Should-* commands.
  2. Assert-MockCalled and Assert-VerifiableMock are gone; mock verification now lives under Should -Invoke, Should -InvokeVerifiable and Should-Invoke.
  3. -ForEach $null and -ForEach @() now fail discovery by default.
  4. Discovery and run now happen per file, so test files need to carry their own discovery-time setup.
  5. Code coverage is profiler-based by default, and `CoverageGutters` output was removed.
  6. Pester 6 runs on Windows PowerShell 5.1 and PowerShell 7.4+.