Custom Assertions
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.
Create the function
Requirements / Interface
Should will automatically pass on values from pipeline to your function as ActualValue
.
This can be an array or a single object depending on whether you register the function with -SupportsArrayInput
.
When Should
calls your custom assertion, it will invoke it with the following parameters:
- ActualValue: The value passed to
Should
to assert. - Negate: Internal switch/bool parameter used to declare if the user called
Should -Not ...
. - CallerSessionState: Used by some internal assertions to invoke code in the user's state. This can usually be ignored.
- Remaining bound parameters, typically matching the parameters your accept in your assertion, like
ExpectedValue
,Because
,MyCustomParameter
etc.
The function should return a object with the following properties:
- Succeeded: Bool result of assert
- FailureMessage: Message to the user explaining why the assert failed, typically in the format
Expected <expected value>, but got <actual value>
. If the function supports the commonly used-Because
parameter, this property should include that message.
If a problem occurs in your function, ex. it was called with invalid parameters, throw an exception. This will also fail the Should
assertion, while making it clear to the user that it's not normal behavior.
Should
accepts the value to assert by pipeline but passes it to the assertion using -ActualValue $ActualValue
. This means ValueFromPipelineByPropertyName
is not available inside your function, but you can write your own logic to work with complex types provided to ActualValue
.
Advanced functions, typically enabled by using [CmdletBinding()]
or [Parameter()]
, disallows arguments by default. In this scenario, make sure to accept all mandatory parameters in your param-block to avoid errors even if you don't use them. They are ActualValue
, Negate
and CallerSessionState
.
Good practices
- Support
-Not
for negative assertions. If not supported, throw an exception when used.- Note that this is passed as
$Negate
to the function.
- Note that this is passed as
- Support
-Because
to let users customize the error message on failure. - Write tests for you custom assertions to make sure they work the way you intended.
Should
will create error records for failed assertions with theFullyQualifiedErrorId
set toPesterAssertionFailed
.
- Provide comment-based help with synopsis and examples so users can find it using
Get-ShouldOperator
.- If you register an operator with a different name than the internal function, specify
-InternalName YourFunctionName
inAdd-ShouldOperator
. This is required forGet-ShouldOperator
to retrieve your comment-bases help.
- If you register an operator with a different name than the internal function, specify
Register function as operator
When your function is ready you need to register it with Pester. See Add-ShouldOperator for help or check out the examples below. Remember:
- If
-Name
is different from the actual function name, make sure to specify-InternalName
with your function name. This is used to retrieve help inGet-ShouldOperator
. - Specify
-SupportsArrayInput
if your function should work with array input, ex.1,2,3 | Should -BeNumber
Example registering a assertion function named Should-BeNumber
:
Add-ShouldOperator -Name BeNumber `
-InternalName 'Should-BeNumber' `
-Test ${function:Should-BeNumber} `
-Alias 'BeNum' `
-SupportsArrayInput
Due to a limitation in PowerShell and the current design on Should
, Pester is limited to a maximum of 32 Should operators. Pester currently includes 25 built-in operators, which limits how many custom operators you can register in a single session.
The limitation is tracked in this issue
Examples
Simple example
#Requires -Module Pester
function Should-BeUpperCaseOnly ([string] $ActualValue, [switch] $Negate, [string] $Because) {
<#
.SYNOPSIS
Checks provided string for only uppercase letters
.EXAMPLE
"ABC" | Should -BeUpperCaseOnly
Checks if "ABC" only contains uppercase letters. This should pass.
.EXAMPLE
"aBc" | Should -Not -BeUpperCaseOnly
Checks if "aBc" does NOT contain only uppercase. Since there are lowercase letters in the string, this should pass.
#>
$arr = $ActualValue.ToCharArray()
[bool] $succeeded = @($arr | Where-Object { [char]::IsLetter($_) -and [char]::IsUpper($_) }).Count -eq $arr.Count
if ($Negate) { $succeeded = -not $succeeded }
if (-not $succeeded) {
if ($Negate) {
$failureMessage = "Expected '$ActualValue' to not only contain uppercase letters$(if($Because) { " because $Because"})."
}
else {
$failureMessage = "Expected '$ActualValue' to only contain uppercase letters$(if($Because) { " because $Because"})."
}
}
return [pscustomobject]@{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
Add-ShouldOperator -Name BeUpperCaseOnly `
-InternalName 'Should-BeUpperCaseOnly' `
-Test ${function:Should-BeUpperCaseOnly} `
-Alias 'BU'
BeforeDiscovery {
# Loads and registers my custom assertion. Ignores usage of unapproved verb with -DisableNameChecking
Import-Module "$PSScriptRoot/MyCustomAssertions.psm1" -DisableNameChecking
}
Describe 'Testing Should -BeUpperCaseOnly' {
It 'Success' {
"HELLO" | Should -BeUpperCaseOnly
}
It 'Failure' {
"HeLLo" | Should -BeUpperCaseOnly -Because "it looks cooler"
}
It 'Not all characters are uppercase' {
"hELLO" | Should -Not -BeUpperCaseOnly
}
It 'We can test failed assertions' {
{ "fails" | Should -BeUpperCaseOnly } | Should -Throw -ErrorId 'PesterAssertionFailed'
}
}
Demo
Invoke-Pester ./demo.tests.ps1
Starting discovery in 1 files.
Discovery found 4 tests in 181ms.
Running tests.
[-] Testing custom assertion.Failure 22ms (20ms|3ms)
Expected 'HeLLo' to only contain uppercase letters because it looks cooler.
at "HeLLo" | Should -BeUpperCaseOnly -Because "it looks cooler", /workspaces/Pester/Samples/demo.tests.ps1:11
at <ScriptBlock>, /workspaces/Pester/Samples/demo.tests.ps1:11
Tests completed in 651ms
Tests Passed: 3, Failed: 1, Skipped: 0 NotRun: 0
SupportsArrayInput example
#Requires -Module Pester
function Should-ContainOnlyValuesOf ($ActualValue, [int]$ExpectedValue, [switch] $Negate, [string] $Because) {
<#
.SYNOPSIS
Asserts if collection contains only the expected value
.EXAMPLE
3,3,3 | Should -ContainOnlyValuesOf 3
Checks if the array contains only values of 3. This should pass.
.EXAMPLE
1,2,3 | Should -Not -ContainOnlyValuesOf 3
Checks if the array contains other values than 3. This should pass.
#>
$notEqual = foreach ($val in $ActualValue) {
if ($val -ne $ExpectedValue) { $val }
}
[bool] $succeeded = @($notEqual).Count -eq 0
if ($Negate) { $succeeded = -not $succeeded }
if (-not $succeeded) {
if ($Negate) {
$failureMessage = "Expected values not equal to $ExpectedValue to be in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})."
}
else {
$failureMessage = "Expected only values equal to $ExpectedValue in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})."
}
}
return [pscustomobject]@{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
Add-ShouldOperator -Name ContainOnlyValuesOf `
-InternalName 'Should-ContainOnlyValuesOf' `
-Test ${function:Should-ContainOnlyValuesOf} `
-SupportsArrayInput
BeforeDiscovery {
# Loads and registers my custom assertion. Ignores usage of unapproved verb with -DisableNameChecking
Import-Module "$PSScriptRoot/MyCustomAssertions.psm1" -DisableNameChecking
}
Describe 'Testing Should -ContainOnlyValuesOf' {
It 'All values are as expected' {
3, 3, 3 | Should -ContainOnlyValuesOf 3
}
It 'Failure' {
3, 3, 3 | Should -Not -ContainOnlyValuesOf 3
}
}
Demo
Invoke-Pester ./demo.tests.ps1
Starting discovery in 1 files.
Discovery found 2 tests in 25ms.
Running tests.
[-] Testing Should -ContainOnlyValuesOf.Failure 8ms (6ms|2ms)
Expected values not equal to 3 to be in collection @(3, 3, 3).
at 3, 3, 3 | Should -Not -ContainOnlyValuesOf 3, /workspaces/Pester/Samples/demo.tests.ps1:11
at <ScriptBlock>, /workspaces/Pester/Samples/demo.tests.ps1:11
Tests completed in 83ms
Tests Passed: 1, Failed: 1, Skipped: 0 NotRun: 0