Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should-invoke and not invoke #2587

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"rollForward": "latestFeature",
"rollForward": "latestMajor",
"version": "8.0.100",
"allowPrerelease": false
}
}
}
3 changes: 3 additions & 0 deletions src/Module.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ $script:SafeCommands['Set-DynamicParameterVariable'] = $ExecutionContext.Session
'Should-BeLikeString'
'Should-NotBeLikeString'

'Should-Invoke'
'Should-NotInvoke'

'Should-BeFasterThan'
'Should-BeSlowerThan'
'Should-BeBefore'
Expand Down
5 changes: 4 additions & 1 deletion src/Pester.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
CompanyName = 'Pester'

# Copyright statement for this module
Copyright = 'Copyright (c) 2024 by Pester Team, licensed under Apache 2.0 License.'
Copyright = 'Copyright (c) 2025 by Pester Team, licensed under Apache 2.0 License.'

# Description of the functionality provided by this module
Description = 'Pester provides a framework for running BDD style Tests to execute and validate PowerShell commands inside of PowerShell and offers a powerful set of Mocking Functions that allow tests to mimic and mock the functionality of any command inside of a piece of PowerShell code being tested. Pester tests can execute any command or script that is accessible to a pester test file. This can include functions, Cmdlets, Modules and scripts. Pester can be run in ad hoc style in a console or it can be integrated into the Build scripts of a Continuous Integration system.'
Expand Down Expand Up @@ -106,6 +106,9 @@
'Should-BeLikeString'
'Should-NotBeLikeString'

'Should-Invoke'
'Should-NotInvoke'

'Should-BeFasterThan'
'Should-BeSlowerThan'
'Should-BeBefore'
Expand Down
38 changes: 19 additions & 19 deletions src/functions/Pester.SessionState.Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ function Should-InvokeVerifiable ([switch] $Negate, [string] $Because) {
Set-ShouldOperatorHelpMessage -OperatorName InvokeVerifiable `
-HelpMessage 'Checks if any Verifiable Mock has not been invoked. If so, this will throw an exception.'

function Should-Invoke {
function Should-InvokeAssertion {
<#
.SYNOPSIS
Checks if a Mocked command has been called a certain number of times
Expand Down Expand Up @@ -966,7 +966,7 @@ function Should-Invoke {

& $script:SafeCommands['Add-ShouldOperator'] -Name Invoke `
-InternalName Should-Invoke `
-Test ${function:Should-Invoke}
-Test ${function:Should-InvokeAssertion}

Set-ShouldOperatorHelpMessage -OperatorName Invoke `
-HelpMessage 'Checks if a Mocked command has been called a certain number of times and throws an exception if it has not.'
Expand Down Expand Up @@ -1135,28 +1135,28 @@ function Invoke-Mock {

# using @() to always get array. This avoids null error in Invoke-MockInternal when no behaviors where found (if-else unwraps the lists)
$behaviors = @(if ($targettingAModule) {
# we have default module behavior add it to the filtered behaviors if there are any
if ($null -ne $moduleDefaultBehavior) {
$moduleBehaviors.Add($moduleDefaultBehavior)
# we have default module behavior add it to the filtered behaviors if there are any
if ($null -ne $moduleDefaultBehavior) {
$moduleBehaviors.Add($moduleDefaultBehavior)
}
else {
# we don't have default module behavior add the default non-module behavior if we have any
if ($null -ne $nonModuleDefaultBehavior) {
$moduleBehaviors.Add($nonModuleDefaultBehavior)
}
}

$moduleBehaviors
}
else {
# we don't have default module behavior add the default non-module behavior if we have any
# we are not targeting a mock in a module use the non module behaviors
if ($null -ne $nonModuleDefaultBehavior) {
$moduleBehaviors.Add($nonModuleDefaultBehavior)
# add the default non-module behavior if we have any
$nonModuleBehaviors.Add($nonModuleDefaultBehavior)
}
}

$moduleBehaviors
}
else {
# we are not targeting a mock in a module use the non module behaviors
if ($null -ne $nonModuleDefaultBehavior) {
# add the default non-module behavior if we have any
$nonModuleBehaviors.Add($nonModuleDefaultBehavior)
}

$nonModuleBehaviors
})
$nonModuleBehaviors
})

$callHistory = (Get-MockDataForCurrentScope).CallHistory

Expand Down
199 changes: 199 additions & 0 deletions src/functions/assert/Mock/Should-Invoke.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
function Should-Invoke {
<#
.SYNOPSIS
Checks if a Mocked command has been called a certain number of times
and throws an exception if it has not.

.DESCRIPTION
This command verifies that a mocked command has been called a certain number
of times. If the call history of the mocked command does not match the parameters
passed to Should-Invoke, Should-Invoke will throw an exception.

.PARAMETER CommandName
The mocked command whose call history should be checked.

.PARAMETER ModuleName
The module where the mock being checked was injected. This is optional,
and must match the ModuleName that was used when setting up the Mock.

.PARAMETER Times
The number of times that the mock must be called to avoid an exception
from throwing.

.PARAMETER Exactly
If this switch is present, the number specified in Times must match
exactly the number of times the mock has been called. Otherwise it
must match "at least" the number of times specified. If the value
passed to the Times parameter is zero, the Exactly switch is implied.

.PARAMETER ParameterFilter
An optional filter to qualify which calls should be counted. Only those
calls to the mock whose parameters cause this filter to return true
will be counted.

.PARAMETER ExclusiveFilter
Like ParameterFilter, except when you use ExclusiveFilter, and there
were any calls to the mocked command which do not match the filter,
an exception will be thrown. This is a convenient way to avoid needing
to have two calls to Should-Invoke like this:

Should-Invoke SomeCommand -Times 1 -ParameterFilter { $something -eq $true }
Should-Invoke SomeCommand -Times 0 -ParameterFilter { $something -ne $true }

.PARAMETER Scope
An optional parameter specifying the Pester scope in which to check for
calls to the mocked command. For RSpec style tests, Should-Invoke will find
all calls to the mocked command in the current Context block (if present),
or the current Describe block (if there is no active Context), by default. Valid
values are Describe, Context and It. If you use a scope of Describe or
Context, the command will identify all calls to the mocked command in the
current Describe / Context block, as well as all child scopes of that block.

.PARAMETER Because
The reason why the mock should be called.

.PARAMETER Verifiable
Makes sure that all verifiable mocks were called.

.EXAMPLE
```powershell
Mock Set-Content {}

{... Some Code ...}

Should-Invoke Set-Content
```

This will throw an exception and cause the test to fail if Set-Content is not called in Some Code.

.EXAMPLE
```powershell
Mock Set-Content -parameterFilter {$path.StartsWith("$env:temp\")}

{... Some Code ...}

Should-Invoke Set-Content 2 { $path -eq "$env:temp\test.txt" }
```

This will throw an exception if some code calls Set-Content on $path=$env:temp\test.txt less than 2 times

.EXAMPLE
```powershell
Mock Set-Content {}

{... Some Code ...}

Should-Invoke Set-Content 0
```

This will throw an exception if some code calls Set-Content at all

.EXAMPLE
Mock Set-Content {}

{... Some Code ...}

Should-Invoke Set-Content -Exactly 2

This will throw an exception if some code does not call Set-Content Exactly two times.

.EXAMPLE
```powershell
Describe 'Should-Invoke Scope behavior' {
Mock Set-Content { }

It 'Calls Set-Content at least once in the It block' {
{... Some Code ...}

Should-Invoke Set-Content -Exactly 0 -Scope It
}
}
```

Checks for calls only within the current It block.

.EXAMPLE
```powershell
Describe 'Describe' {
Mock -ModuleName SomeModule Set-Content { }

{... Some Code ...}

It 'Calls Set-Content at least once in the Describe block' {
Should-Invoke -ModuleName SomeModule Set-Content
}
}
```

Checks for calls to the mock within the SomeModule module. Note that both the Mock
and Should-Invoke commands use the same module name.

.EXAMPLE
```powershell
Should-Invoke Get-ChildItem -ExclusiveFilter { $Path -eq 'C:\' }
```

Checks to make sure that Get-ChildItem was called at least one time with
the -Path parameter set to 'C:\', and that it was not called at all with
the -Path parameter set to any other value.

.NOTES
The parameter filter passed to Should-Invoke does not necessarily have to match the parameter filter
(if any) which was used to create the Mock. Should-Invoke will find any entry in the command history
which matches its parameter filter, regardless of how the Mock was created. However, if any calls to the
mocked command are made which did not match any mock's parameter filter (resulting in the original command
being executed instead of a mock), these calls to the original command are not tracked in the call history.
In other words, Should-Invoke can only be used to check for calls to the mocked implementation, not
to the original.

.LINK
https://pester.dev/docs/commands/Should-Invoke

.LINK
https://pester.dev/docs/assertions
#>
[CmdletBinding(DefaultParameterSetName = 'Default')]
param(
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Default')]
[string]$CommandName,

[Parameter(Position = 1, ParameterSetName = 'Default')]
[int]$Times = 1,

[parameter(ParameterSetName = 'Default')]
[ScriptBlock]$ParameterFilter = { $True },

[Parameter(ParameterSetName = 'Default')]
[Parameter(ParameterSetName = 'ExclusiveFilter', Mandatory = $true)]
[scriptblock] $ExclusiveFilter,

[Parameter(ParameterSetName = 'Default')]
[string] $ModuleName,
[Parameter(ParameterSetName = 'Default')]
[string] $Scope = 0,
[Parameter(ParameterSetName = 'Default')]
[switch] $Exactly,
[Parameter(ParameterSetName = 'Default')]
[Parameter(ParameterSetName = 'Verifiable')]
[string] $Because,

[Parameter(ParameterSetName = 'Verifiable')]
[switch] $Verifiable
)

if ($PSBoundParameters.ContainsKey('Verifiable')) {
$PSBoundParameters.Remove('Verifiable')
$testResult = Should-InvokeVerifiable @PSBoundParameters
Test-AssertionResult $testResult
return
}

# Maps the parameters so we can internally use functions that is
# possible to register as Should operator.
$PSBoundParameters["ActualValue"] = $null
$PSBoundParameters["Negate"] = $false
$PSBoundParameters["CallerSessionState"] = $PSCmdlet.SessionState
$testResult = Should-InvokeAssertion @PSBoundParameters

Test-AssertionResult $testResult
}
Loading
Loading