Pester 6 Is Here: What It Means for the Pester for Beginners Series

PesterForge · July 2026 · 9 min read

📦 Run it yourself — the companion repository pins Pester 6.0.0 and uses the new assertion commands throughout.

Pester 6.0.0 was released on July 7, 2026. That creates an obvious question for anyone who followed my Pester for Beginners series:

Did the entire series become outdated two weeks after I finished it?

Thankfully, no.

Pester 6 builds on the Pester 5 engine. The core model is still there: Describe, Context, It, setup and teardown blocks, mocks, configuration objects, discovery and run phases, tags, test-result files, and the rich result object. Most Pester 5 tests continue to run under Pester 6.

But Pester 6 is not just a version-number change. It introduces a new assertion family, changes a few behaviors that matter in production-grade suites, speeds up coverage, and adds experimental parallel execution.

This post is the bridge between the beginner series and the new Protecting Production with Pester series.

What you will learn

Your Pester 5 knowledge still counts

A Pester 5 test like this still works:

Describe 'Get-ReleaseChannel' {
    It 'returns Production for a production build' {
        Get-ReleaseChannel -BuildType Production | Should -Be 'Production'
    }
}

Pester 6 did not remove the classic Should -Be syntax. You can install Pester 6 and migrate one test file at a time.

That is important because a forced rewrite of every existing test would be an excellent way to create work without creating much value.

The new series will use the Pester 6 assertion style:

Describe 'Get-ReleaseChannel' {
    It 'returns Production for a production build' {
        Get-ReleaseChannel -BuildType Production | Should-Be 'Production'
    }
}

The missing space is intentional:

The new command is not just shorter punctuation. It is a separate, more specialized assertion command.

Why Pester added new assertions

The classic Should command handles many different comparisons through parameter sets. It is flexible, but it often has to guess what kind of comparison you intended.

Pester 6 provides dedicated commands such as:

$value | Should-Be 42
$enabled | Should-BeTrue
$items | Should-BeCollection @('One', 'Two')
$response | Should-BeEquivalent $expected
Should-Invoke Publish-PartnerBatch -Times 1 -Exactly
Should-NotInvoke Remove-PartnerBatch

The dedicated commands can produce more focused failure messages and treat values, collections, strings, dates, and rich objects more deliberately.

For new tests, I think the new syntax is worth using.

For existing tests, I would not rewrite everything simply to make the dash move one character to the left. Migrate when you are already changing the test or when a new assertion solves a real problem.

The collection trap and -Actual

PowerShell pipelines enumerate collections. That can change what an assertion receives.

Consider a one-item array:

$items = @('orders.json')

When you pipe it into a value assertion, PowerShell can unwrap it into a single string. That may be exactly what you intended—or it may hide the fact that your command stopped returning an array.

Pester 6 gives you two useful choices.

Assert the contents as a collection:

$items | Should-BeCollection @('orders.json')

Or preserve the exact object passed to the assertion:

Should-HaveType -Actual $items -Expected ([object[]])

The rule I will use in this series is simple:

When the exact shape matters, use a collection assertion or pass the value through -Actual.

That matters in production automation because the difference between an object and a one-item array can change parameter binding, JSON output, looping behavior, and downstream processing.

Deep object comparison is finally a first-class operation

Production automation returns richer objects than beginner examples usually show. A processing result may include status, identifiers, timestamps, reasons, and nested details.

Pester 6 includes Should-BeEquivalent for recursive comparison:

$actual = [pscustomobject]@{
    BatchId = 'NW-1042'
    Status  = 'Rejected'
    Detail  = [pscustomobject]@{
        Reason = 'UnsupportedSchema'
    }
}

$expected = [pscustomobject]@{
    BatchId = 'NW-1042'
    Status  = 'Rejected'
    Detail  = [pscustomobject]@{
        Reason = 'UnsupportedSchema'
    }
}

$actual | Should-BeEquivalent $expected

You can also exclude volatile fields such as generated IDs or timestamps instead of writing a dozen individual assertions.

That is useful, but do not turn every test into a snapshot of the entire object. If the test only cares that Status is Rejected, assert that. A complete-object comparison makes sense when the complete contract is what you intend to protect.

Discovery and run now happen one file at a time

Pester still has two phases:

  1. Discovery finds the tests.
  2. Run executes them.

In Pester 5, Pester discovered all files before running them. In Pester 6, it discovers and runs each file before moving to the next:

File1.Tests.ps1 -> Discovery -> Run
File2.Tests.ps1 -> Discovery -> Run

Most well-structured suites will not notice.

Poorly isolated suites might.

For example, File2.Tests.ps1 should not assume that File1.Tests.ps1 imported a module, changed the location, or populated a global variable during discovery.

Every test file in the companion repository will be independently executable. Runtime setup belongs in BeforeAll or BeforeEach, not loose at the top of the file or directly inside Describe.

BeforeAll {
    Import-Module "$PSScriptRoot/../src/PartnerFeedGuardian.psd1" -Force
}

Describe 'Partner batch eligibility' {
    It 'rejects an unsupported schema' {
        # Test code belongs here.
    }
}

Static data used to generate tests can be prepared during discovery, but operational actions should not be.

Filtered mocks no longer fall through to reality

This is one of the most important safety changes in Pester 6.

Suppose you create only this mock:

Mock Get-PartnerBatch { 'Test batch' } -ParameterFilter {
    $BatchId -eq 'TEST-1'
}

Then your code unexpectedly calls:

Get-PartnerBatch -BatchId 'PROD-1'

In Pester 5, an unmatched filtered mock could fall through to the real command. That is a terrible surprise when the real command reaches a live API, database, or management system.

Pester 6 fails the call instead when no filtered mock matches and no default mock exists.

For production-sensitive dependencies, I will often add an explicit default mock that throws a message I control:

Mock Get-PartnerBatch {
    throw "Unexpected Get-PartnerBatch call in unit test. BatchId: $BatchId"
}

Mock Get-PartnerBatch {
    [pscustomobject]@{
        BatchId = 'TEST-1'
        Status  = 'Ready'
    }
} -ParameterFilter {
    $BatchId -eq 'TEST-1'
}

The specific mock handles the expected case. The default mock catches everything else.

Parallel execution exists, but it is not the default answer

Pester 6 includes experimental parallel execution at the test-file level. That can shorten large suites, especially when independent test files spend time waiting on separate resources.

It is not something I will enable automatically in this series.

State-changing tests, preflight checks, deployment gates, and production probes often share resources or depend on deliberate ordering. Running them faster is less important than knowing exactly what touched what.

The companion configurations therefore leave parallel execution disabled for production-facing suites.

Later, once the tests are proven independent, a team can choose to enable it for the appropriate files. “Pester supports parallel” is not the same as “this test is safe to parallelize.”

Supported PowerShell versions

Pester 6.0.0 supports:

The new series will use PowerShell 7.4 or later as its primary environment. I will call out Windows PowerShell 5.1 differences when they matter, particularly around modules, classes, remoting, and the production identities that still run legacy Windows automation.

Install and pin Pester 6

For a reproducible project, install and import the exact version:

Install-Module Pester `
    -RequiredVersion 6.0.0 `
    -Scope CurrentUser `
    -Force `
    -SkipPublisherCheck

Remove-Module Pester -ErrorAction SilentlyContinue
Import-Module Pester -RequiredVersion 6.0.0

Get-Module Pester | Select-Object Name, Version, Path

With PSResourceGet, the equivalent installation is:

Install-PSResource Pester -Version 6.0.0 -Scope CurrentUser

The important part is not which installer you prefer. It is that the repository and the pipeline agree on the version being tested.

The version policy for this series

The Protecting Production with Pester series will follow these rules:

  1. Primary examples use Pester 6.0.0.
  2. New assertions use the Should-* syntax.
  3. The companion configurations disable classic Pester 5 assertions so accidental mixing is visible.
  4. Every automated gate returns the Pester result object.
  5. Required gates examine failed, skipped, inconclusive, and unexecuted tests.
  6. Production-facing suites run sequentially by default.
  7. Each test file must be able to run independently.

This is not because Pester 5 became bad. Pester 5 moved into maintenance mode and remains a solid framework. The new series is simply new work, so it should use the current major version deliberately.

Try it yourself

Create Pester6Bridge.Tests.ps1:

Describe 'Pester 6 bridge' {
    It 'uses the new assertion syntax' {
        'Pester 6' | Should-BeString 'Pester 6'
    }

    It 'preserves collection intent' {
        $items = @('manifest.json')
        $items | Should-BeCollection @('manifest.json')
    }

    It 'compares a result object' {
        $actual = [pscustomobject]@{
            Status = 'Approved'
            Reasons = @()
        }

        $expected = [pscustomobject]@{
            Status = 'Approved'
            Reasons = @()
        }

        $actual | Should-BeEquivalent $expected
    }
}

Run it with the pinned version:

Import-Module Pester -RequiredVersion 6.0.0
Invoke-Pester -Path ./Pester6Bridge.Tests.ps1 -Output Detailed

Then change Approved to Rejected and read the failure—not just the red line, but the detail Pester gives you about the mismatch.

Common mistakes

Rewriting every Pester 5 assertion immediately. Existing tests still work. Migrate with purpose, not panic.

Piping a collection when its exact shape is the thing being tested. Use a collection assertion or -Actual.

Assuming a mock proves the real dependency works. A mock proves your code’s behavior against the scenario you supplied.

Sharing hidden state between test files. Pester 6’s per-file discovery and run model rewards independent files and exposes brittle suites.

Turning on parallel execution because it sounds modern. First prove the files are isolated. Then decide whether speed is worth the extra complexity.

Recap

Pester 6 is an evolution of Pester 5, not a reset. Your existing knowledge remains useful, and most existing tests continue to run.

The important additions for this series are the new Should-* assertions, stronger object and collection handling, safer mock behavior, per-file discovery and execution, and a deliberate version policy.

Next up: Part 1 — What Decision Does This Test Control? We will stop treating tests as isolated technical exercises and start mapping each one to a real production decision.

Technical references


← All posts