Download Latest Version 6.0.0 source code.zip (4.4 MB)
Email in envelope

Get an email when there's a new version of Pester

Home / 6.0.0
Name Modified Size InfoDownloads / Week
Parent folder
6.0.0 source code.tar.gz 2026-07-07 4.2 MB
6.0.0 source code.zip 2026-07-07 4.4 MB
README.md 2026-07-07 30.3 kB
Totals: 3 Items   8.7 MB 1

Pester 6.0.0

πŸ™‹ Want to share feedback or report a bug? Open an issue or start a discussion.

Pester 6.0.0 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.4+.

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

:::powershell
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.
  • $Expected drives the comparison type, so 1 | Should-Be $true compares as booleans.
  • Specialized switches such as Should-BeString -IgnoreWhitespace live 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 a whole collection item by item, or finding an ordered sub-collection within one.
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:

:::powershell
1 | Should-Be -Expected 1
Should-Be -Actual 1 -Expected 1

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

:::powershell
# 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[]])

If you forget and pipe a collection into a value or type assertion, the unwrapping is never a silent surprise: when it makes the assertion fail, the message adds a hint that explains what the pipeline did and points you back to -Actual.

:::powershell
[int[]](1, 2) | Should-HaveType ([int[]])
# Expected value to have type [int[]], but got [Object[]] @(1, 2).
#
# Hint: You piped a [int[]] into a type assertion, but the pipeline streams a multi-item
# collection and re-collects it as [Object[]], so the assertion saw [Object[]], not the
# [int[]] you piped. To assert the type of a collection, pass it as the -Actual argument
# instead of piping it, e.g. -Actual $value.

More examples

:::powershell
# 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 @('a', 'c')   # ordered sub-collection, gaps allowed

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

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

:::powershell
$config = New-PesterConfiguration
$config.Should.DisableV5 = $true   # using `Should -Be` now throws

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

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

:::powershell
$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 }) -ExcludePathsNotOnExpected

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

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

:::powershell
$actual | Should-BeEquivalent $expected -Comparator Equality

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

:::powershell
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Parallel = $true
Invoke-Pester -Configuration $config

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

:::powershell
$config.Run.ParallelThrottleLimit = 4   # at most 4 files at a time; 0 (default) uses all processors

Each file is discovered and run inside its own runspace, and the results are merged back into a single [Pester.Run] with correct aggregate counts. Because the files overlap in wall-clock time, the run's total Duration is the orchestrator's measured elapsed time rather than the sum of the files, and the per-phase run totals (user, framework, discovery) are left blank β€” that breakdown is still reported on each container. 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:

powershell $config.Run.BeforeContainer = { . './setup.ps1' } # import modules, dot-source shared setup

If 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):

:::powershell
#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 file-based containers β€” Run.Path as well as New-PesterContainer -Path, including parametrized files built with -Data (each file's data is handed to its worker, so its param() block binds exactly as it does in a serial run). 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 in-memory ScriptBlock containers, 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 via CodeCoverage.UseBreakpoints = $true.
  • Cobertura output is supported in addition to JaCoCo:

powershell $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 .git directory), so reports line up with what CI tools expect.
  • The CoverageGutters output format was removed. It only existed to produce repo-root-relative paths; now that all coverage output is relative to the repo root, plain JaCoCo already works with the Coverage Gutters extension and similar tools. Use JaCoCo (default) or Cobertura.
  • 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.4+.

Richer failure messages and output

  • Single duration is shown per test instead of the user|framework split, for a cleaner line.
  • Array comparisons in Should -Be now 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.
  • Piping a collection into a value or type assertion unwraps it (one item becomes a scalar, several become [object[]]), so the assertion no longer sees your collection. When that makes an assertion fail, the message adds a hint explaining what the pipeline did and pointing you to -Actual.
  • 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), and Output.RenderMode (Auto, Ansi, ConsoleColor, Plaintext) give you fine-grained control over how results render locally and in CI.
  • New Debug.ShowStartMarkers writes 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-MockCalled and Assert-VerifiableMock have been removed. Use Should -Invoke and Should -InvokeVerifiable (classic syntax) or the new Should-Invoke / Should-NotInvoke.
  • Failed Should -Invoke assertions print the recorded mock invocation history, marking which calls matched your -ParameterFilter so you can see why a 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.

Mock invocation history

When a Should -Invoke (or Should-Invoke) assertion fails, Pester now prints the recorded invocation history for the mocked command β€” every recorded call, and whether each one matched your -ParameterFilter β€” so you can see why a filter did or didn't match without adding your own Write-Host debugging. For example, Order.Tests.ps1:

:::powershell
BeforeAll {
    function Send-Email ($To, $Subject) { }
}

Describe 'Order processing' {
    It 'emails alice exactly twice' {
        Mock Send-Email { }

        Send-Email -To 'alice@example.com' -Subject 'Welcome'
        Send-Email -To 'bob@example.com'   -Subject 'Receipt'

        Should -Invoke Send-Email -Times 2 -Exactly -ParameterFilter { $To -eq 'alice@example.com' }
    }
}

The assertion fails because only one of the two calls matched the filter, and the failure lists both recorded calls:

[-] emails alice exactly twice
 Expected Send-Email to be called 2 times exactly, but was called 1 times
 Performed invocations:
   [*] Send-Email -To 'alice@example.com' -Subject 'Welcome' from Order.Tests.ps1:7
   [ ] Send-Email -To 'bob@example.com' -Subject 'Receipt' from Order.Tests.ps1:7
 at Should -Invoke Send-Email -Times 2 -Exactly -ParameterFilter { $To -eq 'alice@example.com' }, Order.Tests.ps1:12

[*] marks a call that matched the parameter filter and [ ] marks one that didn't β€” here alice matched and bob didn't, so the matched count is 1 instead of the expected 2.

Other improvements

  • -TagFilter 'None' runs tests that have no tags. None (case-insensitive) is a reserved filter value that selects tests with no tag on themselves or any parent block. -ExcludeTagFilter 'None' skips those untagged tests so you can run only the tagged ones, and you can combine None with real tags, for example -TagFilter None, Acceptance. See Tags.
  • 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 -ForEach gets $null or @(). Opt out per block/test with -AllowNullOrEmptyForEach.
  • Pester now throws on duplicate BeforeAll/BeforeEach/AfterAll/AfterEach in the same block, catching a common copy-paste mistake.
  • TestResult/CodeCoverage auto-enable when you set any of their non-default options, so you don't silently configure a report that never gets written.
  • Should -Exist gained a -LiteralPath switch.
  • 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.4+.
  • 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.
  • Test discovery now looks inside hidden folders. File search uses Get-ChildItem -Force, so *.Tests.ps1 files in hidden or dot-prefixed folders (for example .config or .build) are now discovered and run; previously they were skipped. Version-control metadata folders (.git, .svn, .hg) are still ignored. Use Run.ExcludePath to skip any folder you don't want picked up.
  • Assert-MockCalled and Assert-VerifiableMock were removed. Use Should -Invoke / Should -InvokeVerifiable.
  • The Pending test status was removed. Set-ItResult no longer has a -Pending parameter; use -Skipped or -Inconclusive instead.
  • The -Focus switch was removed. Describe, Context, and It no longer accept -Focus, and the Focus property is gone from the result object's blocks and tests. Use -Skip, tags, or the Filter configuration to select which tests run.
  • None is now a reserved tag-filter value. -TagFilter 'None' / Filter.Tag = 'None' (and the matching -ExcludeTagFilter / Filter.ExcludeTag) now mean "tests that have no tags" instead of matching a literal tag named None. If you used None as a real tag, filtering by it now also selects β€” or, for the exclude filter, skips β€” every untagged test. Rename the tag if you relied on the old literal match. None is compared case-insensitively.
  • Profiler-based code coverage is the default. Set CodeCoverage.UseBreakpoints = $true to restore breakpoint-based coverage.
  • The CodeCoverage.OutputFormat = 'CoverageGutters' value was removed. All coverage output is now repo-root-relative, so use JaCoCo (the default) or Cobertura.
  • -ForEach $null/@() throws by default (Run.FailOnNullOrEmptyForEach).
  • Duplicate setup/teardown blocks throw instead of being silently allowed.
  • Test and block names: only <...> templates are expanded, everything else stays literal. Names used to be expanded by re-parsing the whole name as a double-quoted string, so a literal backtick, $, $(...) or quote could break the name (It 'has `backticks`' threw a parse error) or be used to run code. Now only the <...> tokens become sub-expressions and every other character is kept inert, which closes that parse-break / code-injection vector (#2044). Inside <...> the content is still evaluated as a PowerShell expression at run time β€” the current -ForEach/-TestCases item and its properties, any in-scope variable, and full expressions such as arithmetic or method calls β€” and the result is rendered through Pester's formatter. This is broader than v5, which substituted only simple data/variable/property references inside <...> and left anything more complex verbatim, so a name like 'adds up to <($a + $b)>' now renders adds up to 3 instead of the literal text. If you want the literal text, escape the leading bracket: `<($a + $b)>.
  • 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:

  1. The classic Should assertions still work, so your existing tests keep running. Adopt Should-* where you want clearer messages, and flip Should.DisableV5 only when you're ready.
  2. Check any -ForEach that can receive an empty collection β€” it now fails discovery unless you set -AllowNullOrEmptyForEach.
  3. Replace Assert-MockCalled / Assert-VerifiableMock with Should -Invoke / Should -InvokeVerifiable.
  4. If you depend on breakpoint-based coverage numbers, set CodeCoverage.UseBreakpoints = $true.
  5. 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 candidates. 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.

Source: README.md, updated 2026-07-07