Contents

What Pester 6.2 Brought


Pester 6.2.0 was released on September 9 — the third feature release in the Pester 6 line in just over two months. The headline is setup files that apply per folder, but a few of the smaller changes might matter more to you day to day, including one that reverses a 6.0 breaking change.

This continues from Pester 6.0 and Pester 6.1. Same demo folder and run.ps1 helper as before, now pinned to 6.2.0:

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

Pester 6 already supported a Pester.BeforeContainer.ps1 file in the root of your repository, for setup that every test file needs (importing the module under test, loading helper functions). In 6.2 that becomes a cascade: every Pester.BeforeContainer.ps1 from the repository root down to the test file’s folder applies, outermost first. Think of it like .editorconfig.

The demo repository looks like this:

repo/
├── Pester.BeforeContainer.ps1           <- imports src/MyModule.psm1, applies to everything
├── src/MyModule.psm1
└── tests/
    ├── Pester.BeforeContainer.ps1       <- defines a New-TestUser helper, applies to tests/ and below
    ├── unit/
    │   ├── Pester.BeforeContainer.ps1   <- $script:Db = 'in-memory'
    │   └── Get-User.Tests.ps1
    ├── integration/
    │   ├── Pester.BeforeContainer.ps1   <- $script:Db = 'real-sql'
    │   └── Get-User.Tests.ps1
    └── docs/
        ├── Pester.BeforeContainer.ps1   <- #pester:no-inherit, $script:Db = 'none'
        └── Docs.Tests.ps1

Each setup file is just a BeforeAll block, for example the root one:

# Applies to every test file under this folder.
BeforeAll {
    Import-Module $PSScriptRoot/src/MyModule.psm1 -Force
}

And the unit tests can use everything above them without a single line of setup of their own:

Describe 'Get-User (unit)' {
    It 'uses the in-memory database' {
        $script:Db | Should-BeString 'in-memory'
    }

    It 'can use helpers from tests/ and the module from the root' {
        (Get-User -Name (New-TestUser -Name 'jane').Name).Name | Should-BeString 'jane'
    }
}

Three details that make this work well:

  • Setup is scoped to the file — it’s dot-sourced into each test file’s own scope, so the unit and integration folders can set the same variable to different values without leaking into each other.
  • #pester:no-inherit at the top of a setup file stops the cascade at that folder, like root = true in .editorconfig. The docs folder above uses it.
  • Each container reports which files applied in a new BeforeContainerFile property, which is handy when you’re wondering why a test does or doesn’t see something.

The cascade starts at Run.RepoRoot, which Pester finds by looking for a .git folder. The demo isn’t a git repository, so the helper script sets it explicitly (run.ps1 resolves -RepoRoot to an absolute path, because a relative one isn’t resolved correctly):

pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/repo/tests -RepoRoot ./v6.2/repo
pwsh -NoProfile -File ./v6.2/show-cascade.ps1
Pester v6.2.0

Running tests from 3 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\docs\Docs.Tests.ps1'
Describing Docs (opted out with #pester:no-inherit)
  [+] does not see the root setup 5.04s

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\integration\Get-User.Tests.ps1'
Describing Get-User (integration)
  [+] uses the real database 19ms

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\unit\Get-User.Tests.ps1'
Describing Get-User (unit)
  [+] uses the in-memory database 22ms
  [+] can use helpers from tests/ and the module from the root 18ms
Tests completed in 6.65s
Tests Passed: 4, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Docs.Tests.ps1 [Passed]
    C:\code\pester-v6-demo\v6.2\repo\tests\docs\Pester.BeforeContainer.ps1
Get-User.Tests.ps1 [Passed]
    C:\code\pester-v6-demo\v6.2\repo\Pester.BeforeContainer.ps1
    C:\code\pester-v6-demo\v6.2\repo\tests\Pester.BeforeContainer.ps1
    C:\code\pester-v6-demo\v6.2\repo\tests\integration\Pester.BeforeContainer.ps1
Get-User.Tests.ps1 [Passed]
    C:\code\pester-v6-demo\v6.2\repo\Pester.BeforeContainer.ps1
    C:\code\pester-v6-demo\v6.2\repo\tests\Pester.BeforeContainer.ps1
    C:\code\pester-v6-demo\v6.2\repo\tests\unit\Pester.BeforeContainer.ps1

The request for this came from Johan Ljunggren (johlju) for the SqlServerDsc test suite, where it’s estimated to remove around a thousand lines of duplicated setup. If you maintain a module with a few dozen test files that all start with the same BeforeAll, this is the reason to update.

Watch out: top-level code in a Pester.BeforeContainer.ps1 now only runs during discovery. If your root setup file has a bare Import-Module line, wrap it in BeforeAll { }, or it won’t be there when your tests run.

Pester 6.0 made duplicate setup and teardown blocks in the same Describe an error. 6.2 reverses that: you can have several BeforeAll, AfterAll, BeforeEach and AfterEach blocks. Setups run in order, teardowns in reverse order.

Describe 'Multiple setup blocks' {
    BeforeAll { $script:steps = @('first') }
    BeforeAll { $script:steps += 'second' }
    AfterAll  { Write-Host 'teardown: first registered' }
    AfterAll  { Write-Host 'teardown: second registered' }

    It 'ran both BeforeAll blocks in order' {
        $script:steps | Should-BeCollection @('first', 'second')
    }
}

On 6.1 and on 6.2:

pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.2/checks/Setup.Tests.ps1
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Setup.Tests.ps1
Pester v6.1.0

Running tests from 1 files.
[-] Discovery in C:\code\pester-v6-demo\v6.2\checks\Setup.Tests.ps1 failed with:
System.Management.Automation.RuntimeException: BeforeAll is already defined in this block. Each block can only have one BeforeAll. Combine the code into a single BeforeAll block.
at New-OneTimeTestSetup, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 1403
at BeforeAll, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 22898
at <ScriptBlock>, C:\code\pester-v6-demo\v6.2\checks\Setup.Tests.ps1: line 4
at New-Block, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 789
at Describe, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 16137
at <ScriptBlock>, C:\code\pester-v6-demo\v6.2\checks\Setup.Tests.ps1: line 2
at <ScriptBlock>, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 3363
at Invoke-File, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 3372
at Invoke-BlockContainer, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 3297
at Invoke-ContainerDiscovery, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 1535
at Invoke-Test, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 2708
at Invoke-Pester<End>, C:\Users\RobertPrüst\AppData\Local\powershell\Modules\Pester\6.1.0\Pester.psm1: line 5570
at <ScriptBlock>, C:\code\pester-v6-demo\run.ps1: line 29
Tests completed in 425ms
Tests Passed: 0, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Container failed: 1
  - C:\code\pester-v6-demo\v6.2\checks\Setup.Tests.ps1
Pester v6.2.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\checks\Setup.Tests.ps1'
Describing Multiple setup blocks
  [+] ran both BeforeAll blocks in order 113ms
teardown: second registered
teardown: first registered
Tests completed in 916ms
Tests Passed: 1, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

At the time of writing, the migration guide still lists duplicate blocks as something you need to fix when moving from v5. On 6.2 you don’t.

Two changes that will save you some head-scratching:

  • Values of the wrong type now throw. Run.Parallel = 'yes' used to be accepted; now you get an error that tells you a bool was expected.
  • Unknown keys produce a warning. A typo like Run.Paralel was silently ignored in 6.0 and 6.1, so the option just didn’t do anything. Now Invoke-Pester tells you about it.

The demo script makes both mistakes, passing the configuration as a hashtable:

'--- wrong type'
try { Invoke-Pester -Configuration @{ Run = @{ Path = $tests; Parallel = 'yes' } } } catch { $_.Exception.Message }

'--- typo in a key'
$null = Invoke-Pester -Configuration @{ Run = @{ Path = $tests; Paralel = $true }; Output = @{ Verbosity = 'None' } }
pwsh -NoProfile -File ./v6.2/checks/config-validation.ps1 -Version 6.2.0
Pester 6.2.0
--- wrong type
Cannot process argument transformation on parameter 'Configuration'. Cannot convert value "System.Collections.Hashtable" to type "PesterConfiguration". Error: "Run.Parallel expects a bool, but got the string 'yes'."
--- typo in a key
WARNING: Ignoring configuration key 'Run.Paralel', there is no such option. Check the spelling, 'Get-Help about_PesterConfiguration' lists all the options.

Should-BeString now shows every region that differs between two strings, with line numbers, instead of only the first difference. Useful when you compare generated files. It only does this for longer strings — more than 10 lines or 120 characters. Shorter strings keep the familiar view with a caret under the first difference, which for a short string is the more precise of the two.

Describe 'Should-BeString diff' {
    It 'shows every differing region' {
        $expected = @(
            'name: demo', 'version: 1.0.0', 'author: Jane', 'license: MIT', 'description: A demo module',
            'tags:', '  - pester', '  - testing', 'dependencies:', '  - name: PSReadLine', '    version: 2.4.5',
            '  - name: Pester', '    version: 6.1.0'
        ) -join "`n"
        $actual = @(
            'name: demo', 'version: 1.0.1', 'author: Jane', 'license: MIT', 'description: A demo module',
            'tags:', '  - pester', '  - testing', 'dependencies:', '  - name: PSReadLine', '    version: 2.4.5',
            '  - name: Pester', '    version: 6.2.0'
        ) -join "`n"
        $actual | Should-BeString $expected
    }
}
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Diff.Tests.ps1
Pester v6.2.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\checks\Diff.Tests.ps1'
Describing Should-BeString diff
  [-] shows every differing region 215ms
   Expected strings to be the same, but they were different.
   String lengths are both 197.
   Expected 13 line(s), actual 13 line(s).
   2 regions differ.
      1  1 |   name: demo
      2    | - version: 1.0.0
         2 | + version: 1.0.1
      3  3 |   author: Jane
      4  4 |   license: MIT
     ...
     11 11 |       version: 2.4.5
     12 12 |     - name: Pester
     13    | -     version: 6.1.0
        13 | +     version: 6.2.0
   at $actual | Should-BeString $expected, C:\code\pester-v6-demo\v6.2\checks\Diff.Tests.ps1:14
Tests completed in 1.19s
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

And when a BeforeAll throws, you now get the actual error and the line it came from, instead of a misleading message about break or continue:

Describe 'Setup that throws' {
    BeforeAll { throw 'the database is down' }
    It 'never runs' { }
}
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Throws.Tests.ps1
Pester v6.2.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\checks\Throws.Tests.ps1'
Describing Setup that throws
[-] Describe Setup that throws failed
  RuntimeException: the database is down
  at <ScriptBlock>, C:\code\pester-v6-demo\v6.2\checks\Throws.Tests.ps1:2
Tests completed in 940ms
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0
BeforeAll \ AfterAll failed: 1
  - Setup that throws

Should-BeFasterThan and Should-BeSlowerThan expect a script block to measure or a timespan to compare. In 6.1 they quietly passed when you gave them anything else — a string, a number, $null. Now they fail:

Describe 'Timing assertions' {
    It 'rejects a string' {
        'a string' | Should-BeFasterThan 1s
    }
}
pwsh -NoProfile -File ./run.ps1 -Version 6.1.0 -Path ./v6.2/checks/Strict.Tests.ps1
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Strict.Tests.ps1
Pester v6.1.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\checks\Strict.Tests.ps1'
Describing Timing assertions
  [+] rejects a string 165ms
Tests completed in 1.14s
Tests Passed: 1, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Pester v6.2.0

Running tests from 1 files.

Running tests from 'C:\code\pester-v6-demo\v6.2\checks\Strict.Tests.ps1'
Describing Timing assertions
  [-] rejects a string 180ms
   Expected a [scriptblock] to measure or a [timespan] to compare, but got [string] 'a string'.
   at 'a string' | Should-BeFasterThan 1s, C:\code\pester-v6-demo\v6.2\checks\Strict.Tests.ps1:4
Tests completed in 1.01s
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

If any of your tests start failing on this after the update, they weren’t testing anything before.

Parallel runs are still experimental, but they no longer need PowerShell 7: 6.2 switched from ForEach-Object -Parallel to a runspace pool, which works on both editions. To try it on 5.1:

powershell.exe -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/repo/tests -RepoRoot ./v6.2/repo -Parallel
Windows PowerShell 5.1 - Pester 6.2.0
Pester v6.2.0

Running tests from 3 files in parallel.

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\docs\Docs.Tests.ps1'
Describing Docs (opted out with #pester:no-inherit)
  [+] does not see the root setup 6.67s

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\integration\Get-User.Tests.ps1'
Describing Get-User (integration)
  [+] uses the real database 111ms

Running tests from 'C:\code\pester-v6-demo\v6.2\repo\tests\unit\Get-User.Tests.ps1'
Describing Get-User (unit)
  [+] uses the in-memory database 111ms
  [+] can use helpers from tests/ and the module from the root 7ms
Tests completed in 9.48s
Tests Passed: 4, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

Parallel runs also no longer silently lose a test file’s results: if a worker fails, that file is reported as failed and the rest of the run continues. The underlying cause of that failure, a race while importing modules in parallel, isn’t fixed yet — and it’s openly described as such in the release notes.

  • Pester imports again on early PowerShell 7.4 patch releases — 6.0 and 6.1 failed there with a PesterConfigurationDeserializer error (#3013).
  • Invoke-Pester keeps its -1 exit code on internal failures, which matters if your pipeline checks it.
  • The VS Code Test Explorer’s discovery run now applies the setup file cascade too.

Update (October 2026): a few issues were reported after this post, mostly around parallel runs. Worth keeping an eye on:

  • #3047 — parallel runs can fail with “Collection was modified” during module import
  • #3044 — warning output now shows up in parallel runs
  • #3054 — a relative Run.RepoRoot isn’t resolved against the current location, so the setup file cascade doesn’t apply. Use an absolute path
  • #3059 — Should-BeCollection doesn’t check item order. A migration gotcha: in v5, Should -Be @(1, 2, 3) did check order
  • #3060 — Invoke-Pester overwrites the caller’s $p and $d variables (this one goes back to 5.2)

Three releases in, Pester 6 is settling down. If you’re starting the move from v5 now, go straight to 6.2 — same rule as always, go for the latest unless it’s broken: the setup file cascade removes a lot of boilerplate, the configuration validation catches typos, and one of the more annoying 6.0 breaking changes is gone again.

Happy testing 😊