From 72a88d11950f63227bac65711707dfe582013626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 11 Aug 2026 19:56:41 +0200 Subject: [PATCH 1/3] Document 6.1.0 features and drop removed Run.BeforeContainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the pages and sections for what 6.1.0 ships: - New "Writing your own assertions (v6)" page for New-ShouldAssertion, and a short version of it on the Should-* page. The v5 custom assertions page now points at it. - New "Shuffled test order" page for Run.Shuffle, Run.ShuffleSeed and the '#pester:no-shuffle' opt-out. - Global mocks (Mock.Global) on the mocking page. - Output.ShowTags on the output page, linked from Tags. - Should-BeString and Should-NotBeString list -NormalizeLineEnding. Also fixes docs that no longer match the code. Run.BeforeContainer was removed in #2859, so the parallel, result object and v5 to v6 pages now describe only the Pester.BeforeContainer.ps1 convention file. Code coverage is collected in parallel runs since #2860, so it is no longer listed as a reason to fall back to the sequential path. The command reference is not regenerated here, that happens after the 6.1.0 release. Until then the two links to the New-ShouldAssertion command page are reported as broken by the build. Fixes #416 🤖 --- docs/assertions/custom-assertions.mdx | 4 + docs/assertions/should-command.mdx | 28 +++- docs/assertions/writing-assertions.mdx | 172 +++++++++++++++++++++++++ docs/migrations/v5-to-v6.mdx | 4 +- docs/usage/mocking.mdx | 42 ++++++ docs/usage/output.mdx | 19 +++ docs/usage/parallel.mdx | 24 ++-- docs/usage/result-object.mdx | 2 +- docs/usage/shuffle.mdx | 81 ++++++++++++ docs/usage/tags.mdx | 4 + sidebars.js | 2 + 11 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 docs/assertions/writing-assertions.mdx create mode 100644 docs/usage/shuffle.mdx diff --git a/docs/assertions/custom-assertions.mdx b/docs/assertions/custom-assertions.mdx index 160c72b7..3fa1ead9 100644 --- a/docs/assertions/custom-assertions.mdx +++ b/docs/assertions/custom-assertions.mdx @@ -5,6 +5,10 @@ description: A guide for creating your own custom operators for more advanced or Pester allows users to create their own `Should`-operators for more advanced assertions. This is done by defining a test-function and registering it with Pester using the guidelines below. +:::tip Writing a `Should-*` assertion instead? +This page covers extending the classic `Should -Be` syntax with `Add-ShouldOperator`. To write your own `Should-*` assertion for the v6 syntax, which needs no registration and has no operator limit, see [Writing your own assertions (v6)](./writing-assertions). +::: + ## Create the function ### Requirements / Interface diff --git a/docs/assertions/should-command.mdx b/docs/assertions/should-command.mdx index cc688c42..65b3e6a6 100644 --- a/docs/assertions/should-command.mdx +++ b/docs/assertions/should-command.mdx @@ -444,8 +444,8 @@ All `Should-*` assertions are listed below, grouped by the kind of check they pe #### String -- [`Should-BeString`](../commands/Should-BeString.mdx) — Asserts that the actual value is equal to the expected string. Supports `-CaseSensitive`, `-IgnoreWhitespace` and `-TrimWhitespace`. -- [`Should-NotBeString`](../commands/Should-NotBeString.mdx) — Asserts that the actual value is not equal to the expected string. +- [`Should-BeString`](../commands/Should-BeString.mdx) — Asserts that the actual value is equal to the expected string. Supports `-CaseSensitive`, `-IgnoreWhitespace`, `-TrimWhitespace` and `-NormalizeLineEnding`. +- [`Should-NotBeString`](../commands/Should-NotBeString.mdx) — Asserts that the actual value is not equal to the expected string. Supports the same switches as `Should-BeString`. - [`Should-BeEmptyString`](../commands/Should-BeEmptyString.mdx) — Asserts that the input is an empty string. - [`Should-NotBeEmptyString`](../commands/Should-NotBeEmptyString.mdx) — Asserts that the input is a string that is not `$null` or empty. - [`Should-NotBeWhiteSpaceString`](../commands/Should-NotBeWhiteSpaceString.mdx) — Asserts that the input is a string that is not `$null`, empty, or whitespace only. @@ -491,6 +491,30 @@ All `Should-*` assertions are listed below, grouped by the kind of check they pe - [`Should-HaveParameter`](../commands/Should-HaveParameter.mdx) — Asserts that a command has the expected parameter, and optionally checks its type, default value, aliases, parameter set, mandatory status, and argument completer. - [`Should-NotHaveParameter`](../commands/Should-NotHaveParameter.mdx) — Asserts that a command does not have the given parameter. +### Writing your own + +The set is not closed. A `Should-*` assertion is an ordinary function, and [`New-ShouldAssertion`](../commands/New-ShouldAssertion) gives yours the same building blocks a built-in one has: pipeline input collection, value formatting, the input-shape hints above, and the failure path that makes soft assertions and mock parameter filters work. + +```powershell +function Should-BeAwesome { + [CmdletBinding()] + param ( + [Parameter(Position = 1, ValueFromPipeline)] $Actual, + [Parameter(Position = 0)] $Expected = 'Awesome', + [string] $Because + ) + + $assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input + $Actual = $assert.Actual() + + if ($Actual -ne $Expected) { + $assert.Fail('Expected , but got .', @{ Expected = $Expected; Because = $Because }) + } +} +``` + +See [Writing your own assertions (v6)](./writing-assertions) for the full walkthrough. + ### Choosing your assertion syntax The classic `Should -Be` assertions still ship and still work, and the new `Should-*` assertions sit alongside them. You can adopt them one test at a time, or mix both styles in the same suite while you migrate. diff --git a/docs/assertions/writing-assertions.mdx b/docs/assertions/writing-assertions.mdx new file mode 100644 index 00000000..353a3243 --- /dev/null +++ b/docs/assertions/writing-assertions.mdx @@ -0,0 +1,172 @@ +--- +id: writing-assertions +title: Writing your own assertions (v6) +description: How to write your own Should-* assertion in Pester v6 using New-ShouldAssertion, so it collects pipeline input, formats values and fails exactly like a built-in assertion. +--- + +The [`Should-*` assertions](./should-command) that ship with Pester v6 are ordinary PowerShell functions. So are yours. There is no operator to register and no 32 operator limit: you write a function named `Should-Something`, and as long as it is loaded, users call it like any built-in assertion. + +The part that is not obvious is everything a built-in assertion does *around* the actual comparison: collecting the value whether it came from the pipeline or from `-Actual`, formatting values the way Pester formats them, printing the hint when someone pipes a collection into a value assertion, and routing the failure through the path that makes [soft assertions](./soft-assertions) and mock `-ParameterFilter` work. + +[`New-ShouldAssertion`](../commands/New-ShouldAssertion) gives you all of that. + +:::tip Looking for the v5 syntax? +This page is about the `Should-*` assertions in v6. To extend the classic `Should -Be` syntax with `Add-ShouldOperator`, see [Custom assertions (v5)](./custom-assertions). +::: + +## A minimal assertion + +Call `New-ShouldAssertion` once at the top of your function, passing your own `$PSCmdlet`, your `-Actual` parameter and `$Input`. Then ask it for the value and call `Fail()` when the check does not hold: + +```powershell title="MyAssertions.psm1" +function Should-BeAwesome { + [CmdletBinding()] + param ( + [Parameter(Position = 1, ValueFromPipeline)] $Actual, + [Parameter(Position = 0)] $Expected = 'Awesome', + [string] $Because + ) + + $assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input + $Actual = $assert.Actual() + + if ($Actual -ne $Expected) { + $assert.Fail('Expected , but got .', @{ Expected = $Expected; Because = $Because }) + } +} +``` + +There is nothing to call when the assertion passes. A passing assertion simply returns without calling `Fail()`. + +It is then used, and fails, like a built-in one: + +```powershell +'Awesome' | Should-BeAwesome # passes +'meh' | Should-BeAwesome -Because 'the docs promised' 'Awesome' +# Expected 'Awesome', because the docs promised, but got 'meh'. +``` + +:::note Pass `$Input` even when you don't expect pipeline input +`-Actual` and `$Input` are both passed in every time. Whichever one the user actually used, the other is empty or `$null`, and `Actual()` returns the right value. This is also what lets Pester tell the two call styles apart when it words the hint. +::: + +## The three parameters + +| Parameter | What to pass | Why | +|---|---|---| +| `-Caller` | your assertion's `$PSCmdlet` | reaches the caller's session state, so soft assertions, `-ErrorAction` and mock parameter filters resolve against the real caller | +| `-Actual` | your `-Actual` parameter | the value when it was passed by parameter | +| `-Buffer` | `$Input` | the values when they arrived by pipeline | + +## Failure messages + +`Fail(message)` takes the message template, and optionally a hashtable of data. The template can contain these tokens: + +| Token | Replaced with | +|---|---| +| `` | the formatted `Expected` value from the data | +| `` | the formatted actual value | +| `` | the type of the expected value | +| `` | the type of the actual value | +| `` | the `Because` reason, worded and punctuated for you | +| `` | any other key you put in the data hashtable | + +`Expected`, `Actual`, `Because` and `Hint` are reserved keys with the meanings above. Every other key becomes a `` token, which is how you get extra values into a message: + +```powershell +$assert.Fail('Expected items, but got in .', + @{ Expected = $Expected; Actual = $count; Because = $Because; Collection = $items }) +``` + +Whether `Fail()` throws immediately or records the failure and lets the test continue is decided by the caller's `-ErrorAction` or by `Should.ErrorAction`, exactly like a built-in assertion. You do not handle that yourself. + +## Input shapes with `-As` + +PowerShell unwraps the pipeline, so `1 | Should-Be` and `@(1) | Should-Be` arrive identically. Assertions therefore have to decide whether a single piped item means a value or a one item collection. `-As` makes that choice, and it also selects the wording of the diagnostic hint printed when the assertion fails: + +| `-As` | Input handling | Use for | +|---|---|---| +| `Scalar` (default) | unrolls a single piped item | value assertions like `Should-Be` | +| `ExactType` | unrolls a single piped item | value assertions that also compare the type | +| `Collection` | keeps the input as a collection | collection assertions like `Should-BeCollection` | +| `CollectionItems` | keeps the input as a collection | collection assertions that report on individual items | +| `None` | unrolls a single piped item, no input hint | structural comparison like `Should-BeEquivalent`, where there is no input-shape mistake to hint about | + +```powershell +$assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input -As Collection +``` + +## Overriding the hint + +By default a failure carries Pester's input-shape hint, the one that points out that a collection was piped into a value assertion. When your assertion knows something more specific about why it failed, pass a `Hint` in the data and it replaces the default. It is printed in the standard `Hint: ` form: + +```powershell +$assert.Fail('Expected an exception with message , but got .', @{ + Expected = $Expected + Actual = $message + Hint = "-ExceptionMessage matches using wildcards (-like). Escape [ ] * ? to match them literally." +}) +``` + +This is how [`Should-Throw`](../commands/Should-Throw) explains that an `-ExceptionMessage` filter failed only because of unescaped wildcard characters. + +## The rest of the helper + +Beyond `Actual()` and `Fail()`, the object has a few methods for the less common cases: + +- `Hint()` returns the diagnostic input hint, or `$null`, when you want to inspect it before deciding how to fail. +- `Format(value)` formats a value the way Pester formats values in assertion messages. +- `EnsureScalar(expected)` returns the value unchanged, or throws when it is a collection. Use it to guard an assertion that only makes sense against a single value. +- `IsCollection(value)` tells you whether Pester treats a value as a collection. + +## Sharing logic between assertions + +When several of your assertions share the same comparison, factor it into a helper and thread the *calling* assertion's `$PSCmdlet` and `$Input` through. Nothing keys off the assertion's name, everything keys off the `$PSCmdlet` you pass as `-Caller`, so the hint, the pipeline detection and the `-ErrorAction` decision stay identical no matter how many wrappers sit in between: + +```powershell +function Invoke-MyEquals { + param ([System.Management.Automation.PSCmdlet] $Cmdlet, $Actual, $Buffer, $Expected) + + $assert = New-ShouldAssertion -Caller $Cmdlet -Actual $Actual -Buffer $Buffer + $value = $assert.Actual() + if ($value -ne $Expected) { + $assert.Fail('Expected but got .', @{ Expected = $Expected }) + } +} + +function Should-Equal { + [CmdletBinding()] + param ([Parameter(ValueFromPipeline)] $Actual, [Parameter(Position = 0)] $Expected) + end { Invoke-MyEquals -Cmdlet $PSCmdlet -Actual $Actual -Buffer $Input -Expected $Expected } +} +``` + +## Good practices + +- Name the function `Should-` so it reads like the built-in assertions and is easy to find. +- Accept `-Because` and pass it in the data, so users can explain why the assertion should hold. +- Put `-Actual` at `Position = 1` and the expected value at `Position = 0`, matching the built-in assertions. +- Provide comment based help with a synopsis and examples, so `Get-Help` works on your assertion. +- Test your assertion, including its failure. A failed `Should-*` assertion produces an error record with the `FullyQualifiedErrorId` set to `PesterAssertionFailed`. + +## Using it in tests + +Import the module that defines your assertions, then use them like any other: + +```powershell title="demo.tests.ps1" +BeforeAll { + Import-Module "$PSScriptRoot/MyAssertions.psm1" -DisableNameChecking +} + +Describe 'Should-BeAwesome' { + It 'passes for the awesome' { + 'Awesome' | Should-BeAwesome + } + + It 'fails for the lame' { + { 'meh' | Should-BeAwesome } | Should-Throw -ErrorId 'PesterAssertionFailed' + } +} +``` + +Custom assertions also work inside a mock `-ParameterFilter` with no extra work, because they go through the same failure path as the built-in ones. diff --git a/docs/migrations/v5-to-v6.mdx b/docs/migrations/v5-to-v6.mdx index c77d8fd1..3f56523c 100644 --- a/docs/migrations/v5-to-v6.mdx +++ b/docs/migrations/v5-to-v6.mdx @@ -73,7 +73,7 @@ In v5 a run had two global phases: Pester discovered **every** file first, build **Symptom.** A test file that relied on something *another* file set up at discovery time fails — for example a module imported at the top of one file, or `-ForEach` data that a different file defined. Under parallel each file is discovered in its own runspace, so it definitely won't see the other file's state. -**Fix.** Make each test file self-contained: do its own discovery-time setup in `BeforeDiscovery`, and import the modules it needs. When you need shared bootstrap for every file, use `Run.BeforeContainer` or a `Pester.BeforeContainer.ps1` file. +**Fix.** Make each test file self-contained: do its own discovery-time setup in `BeforeDiscovery`, and import the modules it needs. When you need shared bootstrap for every file, put it in a `Pester.BeforeContainer.ps1` file in the repository root. ```powershell # Each file does its own discovery-time setup instead of @@ -288,7 +288,7 @@ None of these are required to upgrade — your v5 suite keeps working without th ### Run test files in parallel -The per-file model lets v6 run whole test files concurrently, each in its own runspace, which can cut the wall-clock time of a large suite. It is opt-in and still experimental — enable it with `Run.Parallel = $true`. Files that need shared bootstrap can use `Run.BeforeContainer` (or a `Pester.BeforeContainer.ps1` in the repository root) so each worker starts from a known state. See [Parallel execution](../usage/parallel) for the requirements, sequential fallbacks, and the `#pester:no-parallel` opt-out. +The per-file model lets v6 run whole test files concurrently, each in its own runspace, which can cut the wall-clock time of a large suite. It is opt-in and still experimental — enable it with `Run.Parallel = $true`. Files that need shared bootstrap can use a `Pester.BeforeContainer.ps1` in the repository root, so each worker starts from a known state. See [Parallel execution](../usage/parallel) for the requirements, sequential fallbacks, and the `#pester:no-parallel` opt-out. ### Show which test is currently running diff --git a/docs/usage/mocking.mdx b/docs/usage/mocking.mdx index aa27103f..c776a972 100644 --- a/docs/usage/mocking.mdx +++ b/docs/usage/mocking.mdx @@ -302,6 +302,48 @@ Mock Get-Thing { "default" } # handles everything el Mock Get-Thing { "one" } -ParameterFilter { $Id -eq 1 } # specific case ``` +### Global mocks + +:::warning Experimental +`Mock.Global` is experimental. The option name and the behavior may still change before it is declared stable. +::: + +A normal mock only applies to calls made from the scope where it is defined, or from the module named with `-ModuleName`. To be certain that a command like `Invoke-WebRequest` is never called by any code under test, you have to know every module that might call it and mock it in each one. + +With `Mock.Global` a mock applies to calls of the command from any module or script in the runspace: + +```powershell +$config = New-PesterConfiguration +$config.Run.Path = './tests' +$config.Mock.Global = $true +Invoke-Pester -Configuration $config +``` + +The mock itself is written exactly as it is today, one mock now covers every caller: + +```powershell +Describe 'Get-Data' { + It 'does not reach the network' { + Mock Invoke-WebRequest { '' } + + Get-Data # a function in another module that calls Invoke-WebRequest + + Should -Invoke Invoke-WebRequest -Times 1 + } +} +``` + +A common use is making sure a command never really runs. Mock it to throw, and combine that with `-ParameterFilter` so only the calls you care about are blocked while the rest reach the real command: + +```powershell +# block deleting anything outside TestDrive, from any code under test +Mock Remove-Item { throw 'blocked' } -ParameterFilter { $Path -notlike "$TestDrive*" } +``` + +The mock is removed when the test or block that defined it ends, like any other mock, and it is tied to the run that created it, so it cannot leak into a nested Pester-in-Pester run. + +With the option on, `-ModuleName` is only a hint used to resolve the command, not a scope, so existing mocks keep working unchanged. + ### Mock history When a `Should -Invoke` assertion fails, Pester v6 prints the recorded mock history for the command. It lists every recorded call and marks whether each one matched your parameter filter, so you can see why a filter did or didn't match without adding your own `Write-Host` debugging. diff --git a/docs/usage/output.mdx b/docs/usage/output.mdx index a5e8e86b..943ff804 100644 --- a/docs/usage/output.mdx +++ b/docs/usage/output.mdx @@ -100,6 +100,25 @@ In Pester v5 the duration was split into the time spent in your code and the Pes [+] passes 34ms (26ms|9ms) ``` +### Showing tags + +`Output.ShowTags` appends the tags of each `Describe`, `Context` and `It` to its output line. It makes it easy to see what a `-TagFilter` or `-ExcludeTagFilter` is actually matching: + +```powershell +$conf = New-PesterConfiguration +$conf.Run.Path = '.' +$conf.Output.Verbosity = 'Detailed' +$conf.Output.ShowTags = $true +Invoke-Pester -Configuration $conf +``` + +``` +Describing Get-Planet [Tags: Slow, Unix] + [+] returns all planets [Tags: Fast] 34ms +``` + +Blocks and tests without tags are printed as before. See [Tags](./tags) for how filtering works. + ### Showing when each test starts In a long-running suite it can be hard to tell which test is currently executing, or which one is stuck. Enable `Debug.ShowStartMarkers` to print a marker as each test starts, before its result line is written: diff --git a/docs/usage/parallel.mdx b/docs/usage/parallel.mdx index deb79342..feb6251c 100644 --- a/docs/usage/parallel.mdx +++ b/docs/usage/parallel.mdx @@ -38,19 +38,16 @@ Invoke-Pester -Configuration $config ## Shared per-file setup -Each worker starts from a clean runspace, so anything the parent session would normally provide (imported modules, dot-sourced helpers) is not available unless you set it up. `Run.BeforeContainer` takes one or more script blocks that run before every test file is discovered and run, in both sequential and parallel runs: +Each worker starts from a clean runspace, so anything the parent session would normally provide (imported modules, dot-sourced helpers) is not available unless you set it up. -```powershell -$config = New-PesterConfiguration -$config.Run.Path = './tests' -$config.Run.Parallel = $true -$config.Run.BeforeContainer = { . './setup.ps1' } # import modules, dot-source shared setup -Invoke-Pester -Configuration $config -``` +Pester looks for a single `Pester.BeforeContainer.ps1` in the repository root (`Run.RepoRoot`, found from the nearest `.git` directory) and dot-sources it before every test file is discovered and run, in both sequential and parallel runs: -One script block is usually enough, since it can dot-source as many files as needed. +```powershell title="Pester.BeforeContainer.ps1" +Import-Module "$PSScriptRoot/src/MyModule.psm1" -Force +. "$PSScriptRoot/tests/helpers.ps1" +``` -If you don't set `Run.BeforeContainer`, 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. That gives you a per-repo bootstrap with no configuration. Setting `Run.BeforeContainer` overrides the convention file. +That gives you a per-repo bootstrap with no configuration at all. One file is usually enough, since it can import modules and dot-source as many other files as needed. ## Opting a file out @@ -71,7 +68,6 @@ Parallel execution needs PowerShell 7+ and a file-based run (`Run.Path`). When a - on Windows PowerShell 5.1, - for `ScriptBlock` / `Container` inputs (anything that isn't a file), -- 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`. @@ -79,6 +75,10 @@ Parallel execution needs PowerShell 7+ and a file-based run (`Run.Path`). When a 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 behave the same in both modes. Only the concurrency differs. +## Code coverage + +Code coverage is collected in parallel runs. Each worker measures the same locations, the parent merges the per-worker results with the coverage of any `#pester:no-parallel` files it ran itself, and reports a single set of numbers. Turning on `Run.Parallel` does not cost you your coverage report. + :::note Reserved for follow-ups -Code coverage in parallel (collect per worker, merge in the parent) currently falls back to the sequential path. For mixed runs where only some files opt out, those files' IDE-adapter events currently arrive after the parallel batch. Console output and results are unaffected. +For mixed runs where only some files opt out, those files' IDE-adapter events currently arrive after the parallel batch. Console output and results are unaffected. ::: diff --git a/docs/usage/result-object.mdx b/docs/usage/result-object.mdx index 2b89e11c..d18c2190 100644 --- a/docs/usage/result-object.mdx +++ b/docs/usage/result-object.mdx @@ -250,7 +250,7 @@ Files marked this way run **sequentially, in the parent session, after the paral ### Each file is discovered in isolation -Under parallel, each file is discovered and run in its own clean runspace, so it does **not** see state another file created at discovery time. A file that relied on another file's setup will fail - typically surfacing as a failed container (`Run.FailedContainers`) or a discovery error in the container's `ErrorRecord`. Make each file self-contained, and use `Run.BeforeContainer` (or a `Pester.BeforeContainer.ps1` in the repo root) for shared bootstrap that must run before every file. See [Discovery and Run](./discovery-and-run) and the [v5 to v6 migration guide](../migrations/v5-to-v6#discovery-and-run-now-happen-per-file). +Under parallel, each file is discovered and run in its own clean runspace, so it does **not** see state another file created at discovery time. A file that relied on another file's setup will fail - typically surfacing as a failed container (`Run.FailedContainers`) or a discovery error in the container's `ErrorRecord`. Make each file self-contained, and use a `Pester.BeforeContainer.ps1` in the repo root for shared bootstrap that must run before every file. See [Discovery and Run](./discovery-and-run) and the [v5 to v6 migration guide](../migrations/v5-to-v6#discovery-and-run-now-happen-per-file). ## Stable vs. internal properties diff --git a/docs/usage/shuffle.mdx b/docs/usage/shuffle.mdx new file mode 100644 index 00000000..e5b6bd55 --- /dev/null +++ b/docs/usage/shuffle.mdx @@ -0,0 +1,81 @@ +--- +title: Shuffled test order +description: Pester v6 can shuffle the order test files, blocks and tests run in, so hidden dependencies between tests surface instead of staying lucky. The order is driven by a seed, so a failing run can be repeated exactly. +--- + +:::warning Experimental +Shuffling is experimental in Pester v6. Treat `Run.Shuffle` as opt-in. The option names, the directive name, and the behavior may still change before it is declared stable. +::: + +Tests that quietly depend on running in a fixed order are a common source of "passes on my machine". A test that only works because an earlier test left a file, a variable, or a mock behind keeps passing until someone adds, removes, or renames a test. + +`Run.Shuffle` reorders the run so those dependencies show up as failures. + +## Enabling shuffling + +Set `Run.Shuffle` to `$true`: + +```powershell +$config = New-PesterConfiguration +$config.Run.Path = './tests' +$config.Run.Shuffle = $true +Invoke-Pester -Configuration $config +``` + +Pester reorders: + +- the test files, +- the blocks (`Describe` and `Context`) inside a file, +- the blocks and tests (`It`) inside a block. + +Items are only ever reordered **within their own level**. A test never jumps out of its `Context`, and a `Context` never jumps out of its `Describe`, so the setup and teardown that wrap it still wrap it. `BeforeAll` and `AfterAll` also still run around the real first and last item of each block, whichever item that turns out to be. + +## Repeating a shuffled run + +Every shuffled run is driven by a seed. When `Run.ShuffleSeed` is left at its default of `0`, Pester picks a new seed for the run and prints it at the start: + +``` +Shuffling execution order using seed 1738685315. Set 'Run.ShuffleSeed = 1738685315' to repeat this order. +``` + +Set that seed to replay the same order: + +```powershell +$config = New-PesterConfiguration +$config.Run.Path = './tests' +$config.Run.Shuffle = $true +$config.Run.ShuffleSeed = 1738685315 +Invoke-Pester -Configuration $config +``` + +You don't have to read the seed off the screen. The seed that was actually used is on the result object, so you can capture it after a run: + +```powershell +$r = Invoke-Pester -Configuration $config -PassThru +$r.Configuration.Run.ShuffleSeed.Value # 1738685315, even when you did not set one +``` + +That is the practical loop for CI: run shuffled, and when the build goes red, take the seed out of the log or the result and reproduce the exact order locally. + +## Opting a file out + +Some files are ordered on purpose, for example when sibling `It` blocks build up mock history that adds up across the file. A file keeps its declaration order with a `#pester:no-shuffle` directive, the same way a file opts out of parallel execution: + +```powershell title="Deploy.Tests.ps1" +#pester:no-shuffle + +Describe 'Deploy' { + It 'builds' { } + It 'deploys' { } +} +``` + +The directive is parsed like `#requires`: it is matched only inside a real comment token, never inside a string or here-string, and it may appear anywhere in the file. `# pester:no-shuffle` with a space works too. + +Blocks and tests in a marked file keep declaration order even when `Run.Shuffle` is on. The file itself still takes part in the file order shuffle, only its contents are pinned. + +## Notes and limits + +- Shuffling changes only the order. It does not re-run anything, and it does not isolate tests from each other, so a test that writes to shared state still writes to shared state. +- In a [parallel run](./parallel) each worker shuffles its own slice of files, so the cross-file order is not globally reproducible from the seed. The order inside each file is. +- A `#pester:no-shuffle` file is not automatically a `#pester:no-parallel` file. The two directives are independent, mark a file with both when it needs both. diff --git a/docs/usage/tags.mdx b/docs/usage/tags.mdx index e284df42..571f44b1 100644 --- a/docs/usage/tags.mdx +++ b/docs/usage/tags.mdx @@ -5,6 +5,10 @@ description: Use tags on tests and blocks to categorize code and easily choose w The tag parameter is now available on `Describe`, `Context` and `It` and it is possible to filter tags on any level. You can then use `-TagFilter` and `-ExcludeTagFilter` to run just the tests that you want. +:::tip See which tags are on which test +Set [`Output.ShowTags`](./output#showing-tags) to print the tags next to each `Describe`, `Context` and `It` in the console output, so you can see what your filter is actually matching. +::: + Here you can see an example of a test suite that has acceptance tests and unit tests. Some of the tests are slow, some are flaky, and some only work on Linux. Pester makes running all reliable Windows-compatible acceptance tests as simple as: diff --git a/sidebars.js b/sidebars.js index 9bf724c6..677f69fe 100644 --- a/sidebars.js +++ b/sidebars.js @@ -34,6 +34,7 @@ module.exports = { "usage/test-file-structure", "usage/discovery-and-run", "usage/parallel", + "usage/shuffle", "usage/data-driven-tests", "usage/setup-and-teardown", "usage/tags", @@ -54,6 +55,7 @@ module.exports = { "assertions/should-command", "assertions/should-beequivalent", "assertions/soft-assertions", + "assertions/writing-assertions", "assertions/assertions", "assertions/custom-assertions", ], From 436faed3c98c521997d76b01e55d4c4bdf24f727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 11 Aug 2026 20:09:13 +0200 Subject: [PATCH 2/3] Recommend Assert-* plus a Should-* alias when shipping assertions in a module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should is not an approved verb, so a module that exports Should-* functions makes Import-Module warn every consumer. A manifest with explicit FunctionsToExport does not suppress it, and -DisableNameChecking only moves the problem to the user. Name the function Assert-Something and export a Should-Something alias instead. Aliases are not verb checked, so the module imports quietly and the assertion is still called by the name that reads like an assertion. Nothing in Pester keys off the command name, so it behaves the same through the alias. Also notes why Pester itself does not warn (it adds Should to the verb list from its own assembly, which a script module cannot do), and switches the first example to a .ps1 so it does not show the shape that warns. 🤖 --- docs/assertions/writing-assertions.mdx | 53 ++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/assertions/writing-assertions.mdx b/docs/assertions/writing-assertions.mdx index 353a3243..8827dce5 100644 --- a/docs/assertions/writing-assertions.mdx +++ b/docs/assertions/writing-assertions.mdx @@ -18,7 +18,7 @@ This page is about the `Should-*` assertions in v6. To extend the classic `Shoul Call `New-ShouldAssertion` once at the top of your function, passing your own `$PSCmdlet`, your `-Actual` parameter and `$Input`. Then ask it for the value and call `Fail()` when the check does not hold: -```powershell title="MyAssertions.psm1" +```powershell title="MyAssertions.ps1" function Should-BeAwesome { [CmdletBinding()] param ( @@ -143,19 +143,66 @@ function Should-Equal { ## Good practices -- Name the function `Should-` so it reads like the built-in assertions and is easy to find. +- Name the function `Should-` so it reads like the built-in assertions and is easy to find. When you ship it in a module, see [Shipping your assertions in a module](#shipping-your-assertions-in-a-module) below, because `Should` is not an approved verb. - Accept `-Because` and pass it in the data, so users can explain why the assertion should hold. - Put `-Actual` at `Position = 1` and the expected value at `Position = 0`, matching the built-in assertions. - Provide comment based help with a synopsis and examples, so `Get-Help` works on your assertion. - Test your assertion, including its failure. A failed `Should-*` assertion produces an error record with the `FullyQualifiedErrorId` set to `PesterAssertionFailed`. +## Shipping your assertions in a module + +`Should` is not an approved PowerShell verb. A test file that defines or dot-sources a `Should-*` function is fine, but a **module** that exports one makes `Import-Module` print this to everyone who uses it: + +``` +WARNING: The names of some imported commands from the module 'MyAssertions' include unapproved +verbs that might make them less discoverable. +``` + +A module manifest with an explicit `FunctionsToExport` does not suppress it, and `Import-Module -DisableNameChecking` only moves the problem to your users, who then have to remember the switch and lose name checking for everything else in that import. + +Instead, **name the function with the approved `Assert` verb and export a `Should-*` alias**. Aliases are not verb checked, so the module imports quietly and users still call it by the name that reads like an assertion: + +```powershell title="MyAssertions.psm1" +function Assert-BeAwesome { + [CmdletBinding()] + param ( + [Parameter(Position = 1, ValueFromPipeline)] $Actual, + [Parameter(Position = 0)] $Expected = 'Awesome', + [string] $Because + ) + + $assert = New-ShouldAssertion -Caller $PSCmdlet -Actual $Actual -Buffer $Input + $Actual = $assert.Actual() + + if ($Actual -ne $Expected) { + $assert.Fail('Expected , but got .', @{ Expected = $Expected; Because = $Because }) + } +} + +Set-Alias -Name Should-BeAwesome -Value Assert-BeAwesome +Export-ModuleMember -Function Assert-BeAwesome -Alias Should-BeAwesome +``` + +And in the manifest: + +```powershell title="MyAssertions.psd1" +FunctionsToExport = @('Assert-BeAwesome') +AliasesToExport = @('Should-BeAwesome') +``` + +Nothing in Pester keys off the name of the assertion, everything keys off the `$PSCmdlet` you pass as `-Caller`, so the assertion behaves identically whether it is called as `Assert-BeAwesome` or through the `Should-BeAwesome` alias. + +:::note Why doesn't Pester itself warn? +Pester exports `Should-Be` and friends without a warning because it adds `Should` to PowerShell's internal verb list from its own compiled assembly when it loads. That is not something a script module can do, so use the alias instead. +::: + ## Using it in tests Import the module that defines your assertions, then use them like any other: ```powershell title="demo.tests.ps1" BeforeAll { - Import-Module "$PSScriptRoot/MyAssertions.psm1" -DisableNameChecking + Import-Module "$PSScriptRoot/MyAssertions.psd1" } Describe 'Should-BeAwesome' { From d3a97c1f8ddc9e9e84d099aae0e696111d8c006c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 11 Aug 2026 20:10:57 +0200 Subject: [PATCH 3/3] Ask for Mock.Global feedback and state the v7 intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Say in the admonition that we want Mock.Global to become the default in v7 and that the v6 feedback decides it, that we expect it to change nothing for most suites, and ask for a report either way. 🤖 --- docs/usage/mocking.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/usage/mocking.mdx b/docs/usage/mocking.mdx index c776a972..32f2fc93 100644 --- a/docs/usage/mocking.mdx +++ b/docs/usage/mocking.mdx @@ -304,8 +304,10 @@ Mock Get-Thing { "one" } -ParameterFilter { $Id -eq 1 } # specific case ### Global mocks -:::warning Experimental +:::warning Experimental, and we want your feedback `Mock.Global` is experimental. The option name and the behavior may still change before it is declared stable. + +We would like this to become the default in Pester v7, and the feedback from v6 is what decides that. For most suites we expect turning it on to change nothing at all. If it does change a result for you, or if it changes nothing, please [tell us](https://github.com/pester/Pester/issues/new/choose). Both answers are useful. ::: A normal mock only applies to calls made from the scope where it is defined, or from the module named with `-ModuleName`. To be certain that a command like `Invoke-WebRequest` is never called by any code under test, you have to know every module that might call it and mock it in each one.