# What Pester 6.2 Brought


[Pester 6.2.0](https://github.com/pester/Pester/releases/tag/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.

<!--more-->

This continues from [Pester 6.0](/2026-07-14-pester-v6-released/) and [Pester 6.1](/2026-08-16-pester-v6-1/). Same demo folder and `run.ps1` helper as before, now pinned to 6.2.0:

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

## Setup files that cascade down the folder tree

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:

```text
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:

```powershell
# 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:

```powershell
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):

```powershell
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
```

```text
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](https://github.com/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.

## Multiple BeforeAll blocks are allowed again

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.

```powershell
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:

```powershell
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
```

```text
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](https://pester.dev/docs/migrations/v5-to-v6) still lists duplicate blocks as something you need to fix when moving from v5. On 6.2 you don't.

## Configuration validation

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:

```powershell
'--- 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' } }
```

```powershell
pwsh -NoProfile -File ./v6.2/checks/config-validation.ps1 -Version 6.2.0
```

```text
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.
```

## Better failure output

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

```powershell
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
    }
}
```

```powershell
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Diff.Tests.ps1
```

```text
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`:

```powershell
Describe 'Setup that throws' {
    BeforeAll { throw 'the database is down' }
    It 'never runs' { }
}
```

```powershell
pwsh -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/checks/Throws.Tests.ps1
```

```text
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
```

## Assertions that passed while checking nothing

`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:

```powershell
Describe 'Timing assertions' {
    It 'rejects a string' {
        'a string' | Should-BeFasterThan 1s
    }
}
```

```powershell
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
```

```text
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 on Windows PowerShell 5.1

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
powershell.exe -NoProfile -File ./run.ps1 -Version 6.2.0 -Path ./v6.2/repo/tests -RepoRoot ./v6.2/repo -Parallel
```

```text
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.

## Fixes worth knowing

- **Pester imports again on early PowerShell 7.4 patch releases** — 6.0 and 6.1 failed there with a `PesterConfigurationDeserializer` error ([#3013](https://github.com/pester/Pester/issues/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](https://github.com/pester/Pester/issues/3047) — parallel runs can fail with "Collection was modified" during module import
> - [#3044](https://github.com/pester/Pester/issues/3044) — warning output now shows up in parallel runs
> - [#3054](https://github.com/pester/Pester/issues/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](https://github.com/pester/Pester/issues/3059) — `Should-BeCollection` doesn't check item order. A migration gotcha: in v5, `Should -Be @(1, 2, 3)` did check order
> - [#3060](https://github.com/pester/Pester/issues/3060) — `Invoke-Pester` overwrites the caller's `$p` and `$d` variables (this one goes back to 5.2)

## Wrap

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 😊

