Contents

What Pester 6.1 Brought


Five weeks after Pester 6.0, Pester 6.1.0 was released on August 11. It’s a point release in name, but it fills the obvious gap in the new assertions: writing your own Should-* assertions that behave like the built-in ones.

If you haven’t read the 6.0 post yet, start there — this one assumes you know what the new Should-* assertions are. The next point release has its own post too.

Supported versions are unchanged: Windows PowerShell 5.1 and PowerShell 7.4 and later. The examples below use the same demo folder and run.ps1 helper as the 6.0 post, pinned to 6.1.0:

Install-Module -Name Pester -RequiredVersion 6.1.0 -Scope CurrentUser -Force

In Pester 5 you could add custom assertions to Should -<Operator> through Add-ShouldOperator. For the new assertion style, 6.1 adds New-ShouldAssertion: a helper you call from inside your own function, and it takes care of the pipeline handling, value formatting, -Because, soft assertions and hints, so your assertion behaves exactly like Should-Be and friends.

Sticking with the Format-PersonName function from the getting started post, here’s a Should-BeTitleCase assertion:

function Should-BeTitleCase {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, ValueFromPipeline)] $Actual,
        [string] $Because
    )

    $assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input
    $Actual = $assert.Actual()
    $expected = (Get-Culture).TextInfo.ToTitleCase("$Actual".ToLower())

    if ($Actual -cne $expected) {
        $assert.Fail('Expected <expected>,<because> but got <actual>.', @{ Expected = $expected; Because = $Because })
    }
}

The pattern is always the same: create the assertion object, get the actual value from it (it handles piped input versus -Actual), do your check, and call Fail() with a message template. The <expected>, <because> and <actual> placeholders are filled in and formatted the way Pester formats its own messages.

Used in a test, one passing and one failing on purpose:

Describe 'Format-PersonName' {
    It 'returns a title-cased name' {
        Format-PersonName -FirstName 'ALICE' -LastName 'brown' | Should-BeTitleCase
    }

    It 'fails with a readable message (on purpose)' {
        'alice Brown' | Should-BeTitleCase -Because 'names should be title case'
    }
}
pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.1/CustomAssertion.Tests.ps1
Pester v6.1.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.1\CustomAssertion.Tests.ps1'
Describing Format-PersonName
  [+] returns a title-cased name 129ms
  [-] fails with a readable message (on purpose) 159ms
   Expected 'Alice Brown', because names should be title case, but got 'alice Brown'.
   at 'alice Brown' | Should-BeTitleCase -Because 'names should be title case', C:\code\pester-v6-demo\v6.1\CustomAssertion.Tests.ps1:27
Tests completed in 1.67s
Tests Passed: 1, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

One thing to know if you put assertions like this in a module: Should isn’t an approved PowerShell verb, so exporting Should-* functions gives you the unapproved-verb warning on Import-Module. The release notes recommend naming the function Assert-* and exporting a Should-* alias for it:

Set-Alias -Name Should-BeTitleCase -Value Assert-BeTitleCase
Export-ModuleMember -Function Assert-BeTitleCase -Alias Should-BeTitleCase

A batch of smaller improvements to the assertions that came with 6.0:

  • Should-BeString -NormalizeLineEnding — compares strings regardless of CRLF versus LF. If you’ve ever had a test pass on Windows and fail on a Linux build agent because of line endings, this is for you. (The release candidate called it -NormalizeNewline; the final name is -NormalizeLineEnding.)
  • Should-ContainCollection -IgnoreOrder — checks that a collection contains certain items, in any order.
  • Wildcard hints on Should-Throw -ExceptionMessage — -ExceptionMessage matches with -like, so square brackets in the message are wildcard characters. Pester now tells you when that’s why a match failed.
  • A consistency pass — positions and mandatory parameters were aligned across the assertions. That’s the right move for a young API, but if you call an assertion with positional parameters, check that it still binds the way you expect. Dead code for a Should-BeEquivalent -StrictOrder parameter, which was never usable, was removed.
Describe 'New in 6.1' -Tag 'Assertions' {
    It 'ignores line endings' {
        "a`r`nb" | Should-BeString "a`nb" -NormalizeLineEnding
    }

    It 'ignores order in a sub-collection' {
        1, 2, 3 | Should-ContainCollection @(3, 1) -IgnoreOrder
    }

    It 'explains wildcard characters in an exception message (fails on purpose)' {
        { throw 'value is [1]' } | Should-Throw -ExceptionMessage 'value is [1]'
    }
}

Describe 'Slow things' -Tag 'Slow', 'Integration' {
    It 'is tagged' {
        $true | Should-BeTrue
    }
}

The new Output.ShowTags option shows the tags in the output, which helps when you’re working out why a tag filter included or skipped something:

pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.1/Assertions.Tests.ps1 -ShowTags
Pester v6.1.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.1\Assertions.Tests.ps1'
Describing New in 6.1 [Tags: Assertions]
  [+] ignores line endings 184ms
  [+] ignores order in a sub-collection 39ms
  [-] explains wildcard characters in an exception message (fails on purpose) 186ms
   Expected an exception, with message like 'value is [1]' to be thrown, but the message was 'value is [1]'.
   Hint: -ExceptionMessage matches using wildcards (-like). The messages are identical except for the wildcard characters [ ] * ? in -ExceptionMessage. Escape them with a backtick (`[) or use [System.Management.Automation.WildcardPattern]::Escape() to match them literally.
   at { throw 'value is [1]' } | Should-Throw -ExceptionMessage 'value is [1]', C:\code\pester-v6-demo\v6.1\Assertions.Tests.ps1:11

Describing Slow things [Tags: Slow, Integration]
  [+] is tagged 67ms
Tests completed in 1.74s
Tests Passed: 3, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

Also small but nice: skipped data-driven tests now show their real names (handles foo, handles bar) instead of the raw template (handles <_>) for every case.

This is the one the maintainers explicitly want feedback on. Normally, to mock a command that’s called inside a module, you need -ModuleName on Mock so the mock lands in the module’s scope. With Mock.Global = $true, a mock applies everywhere — your test, the module under test, and any other module that calls the command.

The team wants this to become the default in Pester 7, and the release notes put it plainly: “A report that says ’turned it on, nothing changed’ is just as useful to us.”

To see it, a tiny module with a function that calls Get-Date:

function Get-Today {
    (Get-Date).ToString('yyyy-MM-dd')
}

function Get-Year2000 {
    (Get-Date -Year 2000).Year
}

And a test that mocks Get-Date without -ModuleName:

BeforeAll {
    Import-Module $PSScriptRoot/DemoModule.psm1 -Force
}

AfterAll {
    Remove-Module DemoModule -Force
}

Describe 'Mocking a command used inside a module' {
    It 'sees the mock from inside the module' {
        Mock Get-Date { [datetime]'2026-08-16' }
        $null = Get-Today
        Should-Invoke Get-Date -Times 1 -Exactly
    }

    It 'falls through or fails when the parameter filter does not match' {
        Mock Get-Date { [datetime]'1999-12-31' } -ParameterFilter { $Year -eq 1999 }
        Get-Year2000 | Should-Be 2000
    }
}

Run it without and with global mocks:

pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.1/GlobalMock.Tests.ps1
pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.1/GlobalMock.Tests.ps1 -GlobalMock
Pester v6.1.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.1\GlobalMock.Tests.ps1'
Describing Mocking a command used inside a module
  [-] sees the mock from inside the module 493ms
   Expected Get-Date to be called 1 time exactly, but was called 0 times
   Performed invocations:
     <none>
   at Should-Invoke Get-Date -Times 1 -Exactly, C:\code\pester-v6-demo\v6.1\GlobalMock.Tests.ps1:14
  [+] falls through or fails when the parameter filter does not match 50ms
Tests completed in 1.66s
Tests Passed: 1, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0
Pester v6.1.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.1\GlobalMock.Tests.ps1'
Describing Mocking a command used inside a module
  [+] sees the mock from inside the module 439ms
  [-] falls through or fails when the parameter filter does not match 64ms
   RuntimeException: No mock for command 'Get-Date' matched the call: none of the parameter filters matched, and there is no default mock to fall back to. Add a default mock (e.g. `Mock Get-Date { ... }`) or adjust an existing -ParameterFilter.
   The following parameter filters were evaluated and did not match:
       { $Year -eq 1999 }  bound parameters: Year = 2000
Tests completed in 1.31s
Tests Passed: 1, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

The second test is there because the release notes contradict themselves a little. Pester 6.0 removed mock fall-through — when no parameter filter matches, you get an error instead of the real command. But the 6.1 notes, describing a global mock with a -ParameterFilter, say that “the rest fall through to the real command.”

So I tried it. Without global mocks, the first test fails — the mock in the test doesn’t reach into DemoModule, so Get-Date was called 0 times — and the second test passes because the real Get-Date runs. With Mock.Global on, the first test passes, and the second one fails with the same “No mock for command” error you get in 6.0. So in 6.1.0, unmatched calls to a global mock don’t fall through to the real command either. If you need some calls to behave normally, add a default mock without a filter that does what you need.

Run.Shuffle = $true runs your tests in a random order, which is a good way to find tests that only pass because an earlier test left something behind. The seed is printed at the start of every shuffled run, so you can repeat that exact order with Run.ShuffleSeed, and #pester:no-shuffle at the top of a file keeps it in order.

$config = New-PesterConfiguration
$config.Run.Shuffle = $true

Parallel runs (still experimental) also improved: code coverage is now collected across the parallel workers instead of falling back to a sequential run.

This one isn’t in the 6.1.0 release notes, so it’s easy to miss. Pester 6.0 had a Run.BeforeContainer configuration option, for setup code that runs before each test file:

$config.Run.BeforeContainer = { . './setup.ps1' }

It was removed during the 6.1 alphas, because relative paths in it behaved inconsistently. The file-based convention stays: a Pester.BeforeContainer.ps1 file in the root of your repository. If you used the option, move its contents into that file. (And in 6.2 that convention grew into something more useful.)

To see what happens when the old option is still set:

pwsh -NoProfile -File ./v6.1/legacy-beforecontainer.ps1 -Version 6.0.0
pwsh -NoProfile -File ./v6.1/legacy-beforecontainer.ps1 -Version 6.1.0
Pester 6.0.0

Running tests from 1 files.
setup ran
[+] C:\code\pester-v6-demo\v6.1\Plain.Tests.ps1 1.04s
Tests completed in 1.08s
Tests Passed: 1, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Pester 6.1.0
InvalidOperation: C:\code\pester-v6-demo\v6.1\legacy-beforecontainer.ps1:7
Line |
   7 |  $config.Run.BeforeContainer = { Write-Host 'setup ran' }
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The property 'BeforeContainer' cannot be found on this object. Verify that the property exists and can be set.

Running tests from 1 files.
[+] C:\code\pester-v6-demo\v6.1\Plain.Tests.ps1 752ms (1 test)
Tests completed in 774ms
Tests Passed: 1, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

On 6.0 the setup runs. On 6.1, setting the option throws The property 'BeforeContainer' cannot be found on this object — but that’s a non-terminating error, so the script carries on and Pester runs the tests anyway, without your setup. In a pipeline that doesn’t use $ErrorActionPreference = 'Stop', that red line may be the only hint that something is missing.

One thing worth knowing before you update:

Unknown configuration keys are silently ignored (#2975), so a typo like Run.Paralel simply does nothing.

Update (September 2026): after this post, #3013 was reported: 6.0 and 6.1 fail to import on early PowerShell 7.4 patch releases with Cannot convert "PesterConfigurationDeserializer" from String to Type. Both that and the configuration key issue are fixed in 6.2.

On the v5 side, 5.9.1 was released the same day with a backported mocking fix — the maintenance-mode promise in action.

New-ShouldAssertion is the reason to pick up 6.1 if you write a lot of tests against your own objects; the rest is polish on a young set of assertions. And if you’ve got a spare moment, turn on Mock.Global on one of your suites and tell the maintainers what happened — they asked.

Happy testing 😊