Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/assertions/custom-assertions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 26 additions & 2 deletions docs/assertions/should-command.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <expected>,<because> but got <actual>.', @{ 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.
Expand Down
219 changes: 219 additions & 0 deletions docs/assertions/writing-assertions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---
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.ps1"
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 <expected>,<because> but got <actual>.', @{ 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 |
|---|---|
| `<expected>` | the formatted `Expected` value from the data |
| `<actual>` | the formatted actual value |
| `<expectedType>` | the type of the expected value |
| `<actualType>` | the type of the actual value |
| `<because>` | the `Because` reason, worded and punctuated for you |
| `<key>` | 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 `<key>` token, which is how you get extra values into a message:

```powershell
$assert.Fail('Expected <expected> items,<because> but got <actual> in <collection>.',
@{ 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: <text>` form:

```powershell
$assert.Fail('Expected an exception with message <expected>, but got <actual>.', @{
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 <expected> but got <actual>.', @{ 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-<Something>` 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 <expected>,<because> but got <actual>.', @{ 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.psd1"
}

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.
4 changes: 2 additions & 2 deletions docs/migrations/v5-to-v6.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions docs/usage/mocking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,50 @@ Mock Get-Thing { "default" } # handles everything el
Mock Get-Thing { "one" } -ParameterFilter { $Id -eq 1 } # specific case
```

### Global mocks

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

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 { '<html />' }

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.
Expand Down
Loading