6.0.0-rc1
Pre-releasePester 6.0.0
🙋 Want to share feedback or report a bug? Open an issue
or start a discussion.
This is the first release candidate of Pester 6. It is feature complete for 6.0 and meant for
real-world testing before the final release. Pester 6 builds on the v5 runtime (Discovery & Run, the
configuration object, the rich result object) and focuses on a brand new assertion syntax, faster
code coverage, and an experimental parallel runner.
Pester 6 runs on Windows PowerShell 5.1 and PowerShell 7.2+.
- What's new?
- New
Should-*assertions - Choosing your assertion syntax
- Deep object comparison with
Should-BeEquivalent - Discovery and run now happen per file
- Parallel test execution (experimental)
- Faster code coverage by default, plus Cobertura
- PowerShell 5.1 and 7 only
- Richer failure messages and output
- Mocking changes
- Other improvements
- New
- Breaking changes
- Upgrading from Pester v5
- Thank you
- Questions?
What's new?
New Should-* assertions
The headline feature of Pester 6 is a completely new family of assertions. The
Assert project was merged into Pester and is now shipped as
first-class Should-* commands (note the dash, no space):
Describe 'Get-Planet' {
It 'returns Earth' {
Get-Planet | Should-Be 'Earth'
}
}Why a new syntax?
The classic Should -Be operator is flexible but loosely typed: -Be, -BeExactly, -Contain,
and friends all flow through one command, the left side is always unwrapped by the pipeline, and the
failure messages have to guess what you meant. The new assertions are specialized and
type-aware, which means:
- Clearer, more precise failure messages.
$Expecteddrives the comparison type, so1 | Should-Be $truecompares as booleans.- Specialized switches such as
Should-BeString -IgnoreWhitespacelive where they belong. - A consistent, predictable story for
$null, empty collections, and single-item arrays.
See docs/assertion-types.md for the full design.
The four families
The assertions are grouped by how they treat $Actual and $Expected:
| Family | Examples | Use for |
|---|---|---|
| Value – generic | Should-Be, Should-NotBe, Should-BeGreaterThan, Should-BeSame, Should-BeNull, Should-HaveType |
A single value, compared like the PowerShell operators (Should-Be ≈ -eq). |
| Value – type specific | Should-BeString, Should-MatchString/Should-BeLikeString, Should-BeTrue/Should-BeFalse, Should-BeFalsy/Should-BeTruthy, Should-BeBefore/Should-BeAfter, Should-BeFasterThan/Should-BeSlowerThan |
A value of a known type, with type-specific options. |
| Collection – generic | Should-BeCollection, Should-ContainCollection, Should-NotContainCollection |
Comparing whole collections item by item. |
| Collection – combinator | Should-All, Should-Any |
Asserting a condition across every / any item. |
There are also dedicated assertions for exceptions (Should-Throw), mocks (Should-Invoke,
Should-NotInvoke), command metadata (Should-HaveParameter), hashtable shape
(Should-BeHashtable), and deep object comparison (Should-BeEquivalent).
Pipeline vs. -Actual
The actual value can be provided by the pipeline or by the -Actual parameter:
1 | Should-Be -Expected 1
Should-Be -Actual 1 -Expected 1The pipeline unwraps its input, so a value assertion treats 1 and @(1) the same, and @()
as $null. A collection assertion treats the same input as @(1) and @(). When you need to
preserve the exact value or the concrete collection type (for example [int[]]), use -Actual,
which passes the value through unchanged:
# Value assertion – these all pass:
1 | Should-Be -Expected 1
@(1) | Should-Be -Expected 1
$null | Should-Be -Expected $null
# Collection assertion:
1, 2, 3 | Should-BeCollection @(1, 2, 3)
@() | Should-BeCollection @()
# -Actual preserves the original type:
Should-HaveType -Actual ([int[]](1, 2)) -Expected ([int[]])More examples
# Strings, with type-specific options
' hello ' | Should-BeString 'hello' -TrimWhitespace
'Hello' | Should-BeString 'hello' -CaseSensitive # fails, shows a diff with an arrow marker
# Booleans and null
$true | Should-BeTrue
(Get-Item .) | Should-NotBeNull
$result.Error | Should-BeNull
# Collections
1, 2, 3 | Should-BeCollection @(1, 2, 3)
1, 2, 3 | Should-BeCollection -Count 3
1, 2, 3 | Should-All { $_ -gt 0 }
1, 2, 3 | Should-Any { $_ -gt 2 }
@('a', 'b', 'c') | Should-ContainCollection 'b'
# Exceptions
{ throw 'kaboom' } | Should-Throw -ExceptionMessage 'kaboom'
{ throw [System.InvalidOperationException]::new('nope') } |
Should-Throw -ExceptionType ([System.InvalidOperationException])
# Time
{ Start-Sleep -Milliseconds 10 } | Should-BeFasterThan '100ms'
[datetime]::Now.AddMinutes(11) | Should-BeAfter 10minutes -Ago
[datetime]::Now.AddMinutes(20) | Should-BeAfter -Now
# Command metadata
Get-Command Get-Process | Should-HaveParameter -ParameterName Name -Type ([string[]])Soft assertions
Like Should, the new assertions honor Should.ErrorAction = 'Continue', so you can collect
multiple failures from a single It instead of stopping at the first one:
$config = New-PesterConfiguration
$config.Should.ErrorAction = 'Continue'
Describe 'user' {
It 'has the expected shape' {
$user.Name | Should-Be 'Jakub'
$user.Age | Should-Be 31
$user.City | Should-Be 'Prague'
}
}All three assertions run, and every failure is reported at the end of the test.
Choosing your assertion syntax
The classic Should -Be assertions still ship and still work — the new Should-* assertions are
additive. You can adopt them gradually, or mix both in the same suite while you migrate.
When you are ready to commit to the new style, you can switch the old syntax off so it can't be used
by accident:
$config = New-PesterConfiguration
$config.Should.DisableV5 = $true # using `Should -Be` now throwsDeep object comparison with Should-BeEquivalent
Most assertions compare a single value. Should-BeEquivalent is different: it does a deep,
recursive comparison of two objects, walking nested properties, hashtables, dictionaries, and
collections. It is the right tool for asserting on a whole API response, a configuration object, or
any rich structure in one shot — and it produces a readable, property-by-property diff when the two
sides don't match.
$user = Get-User
$user | Should-BeEquivalent ([pscustomobject]@{
Name = 'Jakub'
Age = 31
Address = [pscustomobject]@{ City = 'Prague'; Country = 'CZ' }
Roles = @('admin', 'user')
})By default the comparison is strict and symmetric: every member on both sides has to match, so an
unexpected extra property on the actual object fails the assertion. Compare like with like — an
object to an object, a hashtable to a hashtable. The two options below relax that strictness in the
ways you'll most often want.
Compare only what's on the expected object — -ExcludePathsNotOnExpected
Reach for this when the actual object is large and you only care about a few fields. Any property
that is not present on the expected object is ignored entirely, so you can assert a subset of
a big object without spelling out everything you don't care about:
$user = Get-User # Name, Age, Id, CreatedAt, LastLogin, PasswordHash, ...
# Passes as long as Name and Age match. The other properties are never even looked at.
$user | Should-BeEquivalent ([pscustomobject]@{ Name = 'Jakub'; Age = 31 }) -ExcludePathsNotOnExpectedThis keeps tests focused and resilient: adding a new field to the object under test won't break an
assertion that never claimed to care about it.
Ignore specific paths — -ExcludePath
When you want a full comparison except for a few volatile fields (generated ids, timestamps),
exclude them by name. Use dot-notation to reach nested members:
$actual | Should-BeEquivalent $expected -ExcludePath 'Id', 'Metadata.Timestamp'Choose the strategy — -Comparator
The default Equivalency compares structure and values recursively. Switch to Equality for a
simple, non-recursive equality check when that is all you need:
$actual | Should-BeEquivalent $expected -Comparator EqualityDiscovery and run now happen per file
This is the most significant change you (mostly) won't see. It is invisible for well-isolated suites,
but it removes a guarantee that some suites quietly relied on.
In Pester v5 a run had two global phases: Pester discovered every file first — executing each
file top-to-bottom in discovery mode to build the whole tree of Describe/Context/It blocks —
and only then ran them all. In v6 the unit of work is a single file: Pester discovers a file
and runs it before moving on to the next, interleaving discovery and execution. This is what makes
parallel possible, and serial runs now follow the same model
so the two behave consistently.
Why it matters. Because there is no longer one global discovery phase that sees every file before
anything runs, discovery-time side effects no longer carry across files:
- A module imported at discovery time in one file (at the top of the file, or inside
BeforeDiscovery) is not guaranteed to be loaded while another file is being discovered — and
under parallel it definitely is not, because each file is discovered in its own runspace. - Anything a file needs in order to be discovered — helper modules, the data behind a
-ForEach/BeforeDiscovery, variables — must be set up by that file, not inherited from a
file that happened to be discovered earlier.
In practice, each test file should be self-contained: import the modules it needs and do its own
discovery-time setup. This was always the recommended style; in v6 it is the model, so suites that
were already isolated need no changes. When you do need shared bootstrap, Run.BeforeContainer and
the Pester.BeforeContainer.ps1 convention file give you one place to set up every file — in both
serial and parallel runs — see below.
This also changes what you see on screen. A run now prints a single Running tests from N files.
banner, then per-file results, then one grand-total summary at the end — the old per-file
Starting discovery in N files. / Discovery found X tests / Running tests. framing is no longer
printed during a normal run (the matching plugin events still fire). A discovery-only run
(Run.SkipRun = $true) still prints the discovery counts.
Plugin and IDE-adapter authors: the per-file ContainerDiscoveryStart/End and
ContainerRunStart/End steps fire for every file, so adapters still get the full per-container
contract. The global steps now fire at interleaved times: DiscoveryStart once up front, RunStart
once before the first file runs, and DiscoveryEnd once after the last file has been discovered
— which, because discovery and run are interleaved, lands late in the run rather than before
execution. When you need the whole tree before anything runs, use the discovery-only
Run.SkipRun = $true pass (what the VS Code Test Explorer uses), which still does one full global
discovery with no run.
Parallel test execution (experimental)
Pester 6 introduces an experimental parallel runner that executes test files concurrently,
one file per runspace, using PowerShell 7+ ForEach-Object -Parallel. On a multi-core machine this
can cut wall-clock time for large suites dramatically (early prototypes saw ~6.5s drop to ~1.2s).
It is a configuration option, not a separate command — enable it with Run.Parallel:
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Parallel = $true
Invoke-Pester -Configuration $configBy default Pester uses every available processor. Cap how many files run at once with
Run.ParallelThrottleLimit, which is passed straight to ForEach-Object -Parallel -ThrottleLimit:
$config.Run.ParallelThrottleLimit = 4 # at most 4 files at a time; 0 (default) uses all processorsEach file is discovered and run inside its own runspace, and the results are merged back into a
single [Pester.Run] with correct aggregate counts and durations. This builds directly on the
per-file discovery model described above: a file is a
self-contained unit of work, which is exactly what lets workers run in isolation.
Shared per-file setup (Run.BeforeContainer). Run.BeforeContainer takes one or more
scriptblocks that run before every test file is discovered and run, in both serial and
parallel runs. Use it to import helper modules or dot-source the setup the session would normally
provide — this matters most in parallel, where each worker starts from a clean runspace:
$config.Run.BeforeContainer = { . './setup.ps1' } # import modules, dot-source shared setupIf you don't set it, Pester looks for a single Pester.BeforeContainer.ps1 in the repository
root (Run.RepoRoot, found from the nearest .git directory) and dot-sources it when present — a
zero-config, per-repo bootstrap. Setting Run.BeforeContainer overrides the convention file.
Opting a file out. A test file can opt out of parallelization with a comment directive parsed
like #requires (it is matched only inside real comment tokens, never inside strings):
#pester:no-parallel
Describe 'integration that must not share the box' {
# ...
}Files marked this way run in the parent session on the normal serial path — with shared session
state and live output — while the other files run in parallel.
What's supported, and what falls back. Parallel execution needs PowerShell 7+ and a
file-based run (Run.Path). When a run can't be parallelized it falls back to a normal sequential
run with a warning so it keeps working unchanged — this happens on Windows PowerShell 5.1, for
ScriptBlock/Container inputs, when CodeCoverage is enabled (coverage is always collected on the
sequential path), when Run.SkipRemainingOnFailure = 'Run' (a cross-file stop-on-failure can't span
runspaces), and when every file opts out with #pester:no-parallel.
Within a parallel run each worker runs silently and the parent replays every file's output in
discovery order, emitting the same plugin-event sequence as a serial run. Console output, the
TestResult report (produced once from the merged result tree), and IDE adapters such as the VS Code
adapter therefore behave the same in both modes — only the concurrency differs.
Reserved for follow-ups: code coverage in parallel (collect per worker, merge in the parent —
today it falls back to sequential), and full event parity for mixed runs (when only some files opt
out, their IDE-adapter events currently arrive after the parallel batch; console output and results
are unaffected).
Treat Run.Parallel as opt-in and experimental. The directive name, config shape, and behavior may
still change before it is declared stable.
Faster code coverage by default, plus Cobertura
-
Profiler-based coverage is now the default. Pester used to set a breakpoint on every command;
it now uses the same tracer the Profiler uses, which is much faster on large code bases. The old
behavior is still available viaCodeCoverage.UseBreakpoints = $true. -
Cobertura output is supported in addition to JaCoCo:
$config = New-PesterConfiguration $config.CodeCoverage.Enabled = $true $config.CodeCoverage.OutputFormat = 'Cobertura' # or 'JaCoCo' (default)
-
Coverage paths are now reported relative to the repository root (
Run.RepoRoot, found from the
.gitdirectory), so reports line up with what CI tools expect. -
The
CoverageGuttersoutput format was removed. It only existed to produce repo-root-relative
paths; now that all coverage output is relative to the repo root, plainJaCoCoalready works
with the Coverage Gutters extension and similar tools. UseJaCoCo(default) orCobertura. -
CodeCoverage.ExcludeTests(on by default) keeps your test files out of the coverage numbers, and
you can exclude specific functions from coverage with an attribute.
PowerShell 5.1 and 7 only
Support for PowerShell 3, 4, 6, and early/unsupported 7 has been removed — all of these are out
of support from Microsoft. Dropping them let us delete a large amount of compatibility code, move the
C# to .NET 8 (with net462 for Windows PowerShell 5.1), and modernize the runtime. Pester 6
targets Windows PowerShell 5.1 and PowerShell 7.2+.
Richer failure messages and output
- Single duration is shown per test instead of the
user|frameworksplit, for a cleaner line. - Array comparisons in
Should -Benow point at the first differing index and show the shape of
the input, instead of a flat "expected/but got". - String diffs show an arrow marker at the first differing character.
- Control characters and ANSI/VT sequences in values are escaped (using Unicode Control
Pictures) so they can't corrupt the console or the NUnit/JUnit report. Output.StackTraceVerbosity(None,FirstLine,Filtered,Full),Output.CIFormat
(Auto,AzureDevops,GithubActions), andOutput.RenderMode(Auto,Ansi,ConsoleColor,
Plaintext) give you fine-grained control over how results render locally and in CI.- New
Debug.ShowStartMarkerswrites an indication when each test starts. - Test result reports honor the configured
OutputEncoding, and NUnit3 joins NUnit 2.5 and
JUnit as an output format. - Values interpolated into data-driven test names via
<...>now render through Pester's formatter,
so arrays, hashtables, and objects read nicely in the test name.
Mocking changes
Assert-MockCalledandAssert-VerifiableMockhave been removed. UseShould -Invokeand
Should -InvokeVerifiable(classic syntax) or the newShould-Invoke/Should-NotInvoke.- Mock history can be printed to help you debug why a parameter filter did or didn't match.
- Dynamic parameters are handled more robustly: aliases are matched in
-ParameterFilter, and
mocking gracefully falls back when a command can't produce dynamic parameters. - The implicit "fall through to the real command" behavior was removed for more predictable mocks.
Other improvements
Run.SkipRemainingOnFailure(None,Run,Container,Block) skips the rest of a scope once a
test fails, and remaining tests are skipped when a block setup/teardown fails.Run.FailOnNullOrEmptyForEach(on by default) fails discovery when-ForEachgets$nullor
@(). Opt out per block/test with-AllowNullOrEmptyForEach.- Pester now throws on duplicate
BeforeAll/BeforeEach/AfterAll/AfterEachin the same
block, catching a common copy-paste mistake. TestResult/CodeCoverageauto-enable when you set any of their non-default options, so you don't
silently configure a report that never gets written.Should -Existgained a-LiteralPathswitch.- Hidden folders are included in test discovery on Linux, and a stray write to the success stream in
a setup block no longer crashes the whole run.
Breaking changes
- PowerShell 3, 4, 6, and unsupported 7 are no longer supported. Minimum is Windows PowerShell
5.1 / PowerShell 7.2+. - Discovery and run now happen per file instead of discovering every file up front and then
running everything. It is invisible for self-contained files, but discovery-time side effects (for
example a module imported at the top of one file) no longer carry into another file's discovery.
See Discovery and run now happen per file. Assert-MockCalledandAssert-VerifiableMockwere removed. UseShould -Invoke/
Should -InvokeVerifiable.- The
Pendingtest status was removed.Set-ItResultno longer has a-Pendingparameter;
use-Skippedor-Inconclusiveinstead. - Profiler-based code coverage is the default. Set
CodeCoverage.UseBreakpoints = $trueto
restore breakpoint-based coverage. - The
CodeCoverage.OutputFormat = 'CoverageGutters'value was removed. All coverage output is
now repo-root-relative, so useJaCoCo(the default) orCobertura. -ForEach $null/@()throws by default (Run.FailOnNullOrEmptyForEach).- Duplicate setup/teardown blocks throw instead of being silently allowed.
- Test and block names expand data templates only.
<...>tokens inDescribe/Context/It
names (and in-ForEach/-TestCasesdata) now interpolate only the current data item and its
properties — not arbitrary PowerShell expressions. This closes a code-injection vector where an
expression embedded in a name could execute during discovery. If you relied on full expression
expansion in a name, compute the value into a-ForEachproperty and reference that instead. - The deprecated Legacy parameter set and other long-deprecated functions were removed from
Invoke-Pester. - Mock fall-through to the real command was removed.
Upgrading from Pester v5
For most suites, upgrading is low risk:
- The classic
Shouldassertions still work, so your existing tests keep running. AdoptShould-*
where you want clearer messages, and flipShould.DisableV5only when you're ready. - Check any
-ForEachthat can receive an empty collection — it now fails discovery unless you set
-AllowNullOrEmptyForEach. - Replace
Assert-MockCalled/Assert-VerifiableMockwithShould -Invoke/
Should -InvokeVerifiable. - If you depend on breakpoint-based coverage numbers, set
CodeCoverage.UseBreakpoints = $true. - Remove any duplicate
BeforeAll/AfterEach/etc. in a single block.
If you are still on Pester v4 syntax, see the
migration guide for the v4 → v5 step first. The
configuration object and the test-authoring API are unchanged in v6; the one under-the-hood shift to
be aware of is that discovery and run now happen per file.
Thank you
Pester 6 is the work of many contributors across the 6.0 alphas and release candidate. Special thanks
to the new contributors during the 6.0 cycle, including @kborowinski, @WithHolm, @ocalvo, @joeskeen,
and @johlju, and to everyone who reported issues, tested prereleases, and sponsors Pester.
Questions?
Open an issue, start a
discussion, read the docs at
pester.dev, or find us in #testing
on the PowerShell Slack.