-
-
Notifications
You must be signed in to change notification settings - Fork 470
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
WIP - Block original command by default when parameterized mocks exist #2547
base: main
Are you sure you want to change the base?
Changes from 6 commits
6f724b4
d48aedf
de75df7
8cb47e5
c101907
31f34bf
22ef754
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,20 +28,22 @@ | |
[Parameter(Mandatory)] | ||
$Hook, | ||
[string[]]$RemoveParameterType, | ||
[string[]]$RemoveParameterValidation | ||
[string[]]$RemoveParameterValidation, | ||
[switch]$AllowFallback | ||
) | ||
|
||
[PSCustomObject] @{ | ||
CommandName = $ContextInfo.Command.Name | ||
ModuleName = $ContextInfo.TargetModule | ||
Filter = $ParameterFilter | ||
IsDefault = $null -eq $ParameterFilter | ||
IsInModule = -not [string]::IsNullOrEmpty($ContextInfo.TargetModule) | ||
Verifiable = $Verifiable | ||
Executed = $false | ||
ScriptBlock = $MockWith | ||
Hook = $Hook | ||
PSTypeName = 'MockBehavior' | ||
CommandName = $ContextInfo.Command.Name | ||
ModuleName = $ContextInfo.TargetModule | ||
Filter = $ParameterFilter | ||
AllowFallback = $AllowFallback | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name suggestions for parameter and property? Fallback is also used for "fallback to default mock". Focus on other review comments first in case this design is discarded. |
||
IsDefault = $null -eq $ParameterFilter | ||
IsInModule = -not [string]::IsNullOrEmpty($ContextInfo.TargetModule) | ||
Verifiable = $Verifiable | ||
Executed = $false | ||
ScriptBlock = $MockWith | ||
Hook = $Hook | ||
PSTypeName = 'MockBehavior' | ||
} | ||
} | ||
|
||
|
@@ -982,6 +984,10 @@ | |
|
||
$foundDefaultBehavior = $false | ||
$defaultBehavior = $null | ||
# A mock exists so we do not allow original command to be called. Either: | ||
# - A parameterized mock explicitly allows it below | ||
# - Or a default mock exists which takes precedence either way | ||
$fallbackAllowed = $false | ||
foreach ($b in $Behaviors) { | ||
|
||
if ($b.IsDefault -and -not $foundDefaultBehavior) { | ||
|
@@ -1005,6 +1011,8 @@ | |
} | ||
return $b | ||
} | ||
|
||
if ($b.AllowFallback) { $fallbackAllowed = $true } | ||
} | ||
} | ||
|
||
|
@@ -1018,6 +1026,11 @@ | |
if ($PesterPreference.Debug.WriteDebugMessages.Value) { | ||
Write-PesterDebugMessage -Scope Mock "No parametrized or default behaviors were found filter." | ||
} | ||
|
||
if (-not $fallbackAllowed) { | ||
throw 'Not implemented. None of the parameterized mocks match the call or accept fallback to original command.' | ||
} | ||
|
||
return $null | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,6 +124,10 @@ function Mock { | |
validation requirements, and allows functions that are strict about their | ||
parameter validation to be mocked more easily. | ||
|
||
.PARAMETER AllowFallback | ||
Allows original command to execute when only mocks with -ParameterFilter exists and the call does not match any of them. | ||
Creating a default mock (no -ParameterFilter specified) will take precedence over this. | ||
|
||
.EXAMPLE | ||
Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } | ||
|
||
|
@@ -219,15 +223,22 @@ function Mock { | |
.LINK | ||
https://pester.dev/docs/usage/mocking | ||
#> | ||
[CmdletBinding()] | ||
[CmdletBinding(DefaultParameterSetName = 'Default')] | ||
# TODO: Breaking change due to parameter sets. Previously had positions: | ||
# CommandName, MockWith, ParameterFilter, ModuleName, RemoveParameterType, RemoveParameterValidation | ||
param( | ||
[Parameter(Position = 0)] | ||
[string]$CommandName, | ||
[Parameter(Position = 1)] | ||
[ScriptBlock]$MockWith = {}, | ||
[switch]$Verifiable, | ||
[Parameter(ParameterSetName = 'WithParameterFilter')] | ||
[ScriptBlock]$ParameterFilter, | ||
[string]$ModuleName, | ||
[string[]]$RemoveParameterType, | ||
[string[]]$RemoveParameterValidation | ||
[string[]]$RemoveParameterValidation, | ||
[Parameter(ParameterSetName = 'WithParameterFilter')] | ||
[switch]$AllowFallback | ||
Comment on lines
+226
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to support all original positional parameters or is |
||
) | ||
if (Is-Discovery) { | ||
# this is to allow mocks in between Describe and It which is discouraged but common | ||
|
@@ -280,7 +291,7 @@ function Mock { | |
$mockData.Behaviors[$contextInfo.Command.Name] = $behaviors | ||
} | ||
|
||
$behavior = New-MockBehavior -ContextInfo $contextInfo -MockWith $MockWith -Verifiable:$Verifiable -ParameterFilter $ParameterFilter -Hook $hook | ||
$behavior = New-MockBehavior -ContextInfo $contextInfo -MockWith $MockWith -Verifiable:$Verifiable -ParameterFilter $ParameterFilter -Hook $hook -AllowFallback:$AllowFallback | ||
if ($PesterPreference.Debug.WriteDebugMessages.Value) { | ||
Write-PesterDebugMessage -Scope Mock -Message "Adding a new $(if ($behavior.IsDefault) {"default"} else {"parametrized"}) behavior to $(if ($behavior.ModuleName) { "$($behavior.ModuleName) - "})$($behavior.CommandName)." | ||
} | ||
|
@@ -1045,7 +1056,7 @@ function Invoke-Mock { | |
param ($b) | ||
" Target module: $(if ($b.ModuleName) { $b.ModuleName } else { '$null' })`n" | ||
" Body: { $($b.ScriptBlock.ToString().Trim()) }`n" | ||
" Filter: $(if (-not $b.IsDefault) { "{ $($b.Filter.ToString().Trim()) }" } else { '$null' })`n" | ||
" Filter$(if ($b.AllowFallback) { ' (with fallback)' }): $(if (-not $b.IsDefault) { "{ $($b.Filter.ToString().Trim()) }" } else { '$null' })`n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably update text and/or use separate line. |
||
" Default: $(if ($b.IsDefault) { '$true' } else { '$false' })`n" | ||
" Verifiable: $(if ($b.Verifiable) { '$true' } else { '$false' })" | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -374,7 +374,7 @@ Describe 'When calling Mock, StrictMode is enabled, and variables are used in th | |
|
||
Describe "When calling Mock on existing function without matching bound params" { | ||
It "Should redirect to real function" { | ||
Mock FunctionUnderTest { return "fake results" } -parameterFilter { $param1 -eq "test" } | ||
Mock FunctionUnderTest { return "fake results" } -parameterFilter { $param1 -eq "test" } -AllowFallback | ||
$result = FunctionUnderTest "badTest" | ||
$result | Should -Be "I am a real world test" | ||
} | ||
|
@@ -390,7 +390,7 @@ Describe "When calling Mock on existing function with matching bound params" { | |
|
||
Describe "When calling Mock on existing function without matching unbound arguments" { | ||
It "Should redirect to real function" { | ||
Mock FunctionUnderTestWithoutParams { return "fake results" } -parameterFilter { $param1 -eq "test" -and $args[0] -eq 'notArg0' } | ||
Mock FunctionUnderTestWithoutParams { return "fake results" } -parameterFilter { $param1 -eq "test" -and $args[0] -eq 'notArg0' } -AllowFallback | ||
$result = FunctionUnderTestWithoutParams -param1 "test" "arg0" | ||
$result | Should -Be "I am a real world test with no params" | ||
} | ||
|
@@ -518,15 +518,15 @@ Describe 'When calling Mock on a module-internal function.' { | |
BeforeAll { | ||
Mock -ModuleName TestModule InternalFunction { 'I am the mock test' } | ||
Mock -ModuleName TestModule Start-Sleep { } | ||
Mock -ModuleName TestModule2 InternalFunction -ParameterFilter { $args[0] -eq 'Test' } { | ||
Mock -ModuleName TestModule2 InternalFunction -ParameterFilter { $args[0] -eq 'Test' } -AllowFallback { | ||
"I'm the mock who's been passed parameter Test" | ||
} | ||
# Mock -ModuleName TestModule2 InternalFunction2 { | ||
# # this does not work in v5 because we are running the mock in the test scope | ||
# # so this module internal function is not accessible to the mock body | ||
# InternalFunction 'Test' | ||
# } | ||
Mock -ModuleName TestModule2 Get-CallerModuleName -ParameterFilter { $false } | ||
Mock -ModuleName TestModule2 Get-CallerModuleName -ParameterFilter { $false } -AllowFallback | ||
Mock -ModuleName TestModule2 Get-Content { } | ||
} | ||
|
||
|
@@ -632,7 +632,7 @@ Describe "When Applying multiple Mocks on a single command where one has no filt | |
Describe "When Creating Verifiable Mock that is not called" { | ||
Context "In the test script's scope" { | ||
It "Should throw" { | ||
Mock FunctionUnderTest { return "I am a verifiable test" } -Verifiable -parameterFilter { $param1 -eq "one" } | ||
Mock FunctionUnderTest { return "I am a verifiable test" } -Verifiable -parameterFilter { $param1 -eq "one" } -AllowFallback | ||
FunctionUnderTest "three" | Out-Null | ||
$result = $null | ||
try { | ||
|
@@ -1952,9 +1952,9 @@ Describe 'Mocking a function taking input from pipeline' { | |
$noMockResultByProperty = $psobj | PipelineInputFunction -PipeStr 'val' | ||
$noMockArrayResultByProperty = $psArrayobj | PipelineInputFunction -PipeStr 'val' | ||
|
||
Mock PipelineInputFunction { write-output 'mocked' } -ParameterFilter { $PipeStr -eq 'blah' } | ||
Mock PipelineInputFunction { write-output 'mocked' } -ParameterFilter { $PipeStr -eq 'blah' } -AllowFallback | ||
} | ||
context 'when calling original function with an array' { | ||
Context 'when calling original function with an array' { | ||
BeforeAll { | ||
$result = @(1, 2) | PipelineInputFunction | ||
} | ||
|
@@ -1967,7 +1967,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling original function with an int' { | ||
Context 'when calling original function with an int' { | ||
BeforeAll { | ||
$result = 1 | PipelineInputFunction | ||
} | ||
|
@@ -1978,7 +1978,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling original function with a string' { | ||
Context 'when calling original function with a string' { | ||
BeforeAll { | ||
$result = '1' | PipelineInputFunction | ||
} | ||
|
@@ -1989,7 +1989,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling original function and pipeline is bound by property name' { | ||
Context 'when calling original function and pipeline is bound by property name' { | ||
BeforeAll { | ||
$result = $psobj | PipelineInputFunction -PipeStr 'val' | ||
} | ||
|
@@ -2001,7 +2001,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling original function and forcing a parameter binding exception' { | ||
Context 'when calling original function and forcing a parameter binding exception' { | ||
BeforeAll { | ||
Mock PipelineInputFunction { | ||
if ($MyInvocation.ExpectingInput) { | ||
|
@@ -2017,7 +2017,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling original function and pipeline is bound by property name with array values' { | ||
Context 'when calling original function and pipeline is bound by property name with array values' { | ||
BeforeAll { | ||
$result = $psArrayobj | PipelineInputFunction -PipeStr 'val' | ||
} | ||
|
@@ -2029,7 +2029,7 @@ Describe 'Mocking a function taking input from pipeline' { | |
} | ||
} | ||
|
||
context 'when calling the mocked function' { | ||
Context 'when calling the mocked function' { | ||
BeforeAll { | ||
$result = 'blah' | PipelineInputFunction | ||
} | ||
|
@@ -2289,7 +2289,7 @@ Describe 'Nested Mock calls' { | |
BeforeAll { | ||
$testDate = New-Object DateTime(2012, 6, 13) | ||
|
||
Mock Get-Date -ParameterFilter { $null -eq $Date } { | ||
Mock Get-Date -ParameterFilter { $null -eq $Date } -AllowFallback { | ||
Get-Date -Date $testDate -Format o | ||
} | ||
} | ||
|
@@ -2452,7 +2452,7 @@ Describe "Mocking Set-Variable" { | |
# we mock the command but the mock will never be triggered because | ||
# the filter will never pass, so this mock will always call through | ||
# to the real Set-Variable | ||
Mock Set-Variable -ParameterFilter { $false } | ||
Mock Set-Variable -ParameterFilter { $false } -AllowFallback | ||
|
||
Set-Variable -Name v2 -Value 10 | ||
|
||
|
@@ -2467,7 +2467,7 @@ Describe "Mocking Set-Variable" { | |
Set-Variable -Name v1 -Value 1 | ||
$v1 | Should -Be 1 -Because "we defined it without mocking Set-Variable" | ||
|
||
Mock Set-Variable -ParameterFilter { $false } | ||
Mock Set-Variable -ParameterFilter { $false } -AllowFallback | ||
|
||
Set-Variable -Name v2 -Value 11 -Scope 0 | ||
$v2 | Should -Be 11 | ||
|
@@ -2478,7 +2478,7 @@ Describe "Mocking Set-Variable" { | |
Set-Variable -Name v1 -Value 1 | ||
$v1 | Should -Be 1 -Because "we defined it without mocking Set-Variable" | ||
|
||
Mock Set-Variable -ParameterFilter { $false } | ||
Mock Set-Variable -ParameterFilter { $false } -AllowFallback | ||
|
||
Set-Variable -Name v2 -Value 12 -Scope Local | ||
|
||
|
@@ -2509,7 +2509,7 @@ Describe "Mocking Set-Variable" { | |
& { | ||
# scope 1 | ||
& { | ||
Mock Set-Variable -ParameterFilter { $false } | ||
Mock Set-Variable -ParameterFilter { $false } -AllowFallback | ||
Set-Variable -Name v2 -Value 11 -Scope 3 | ||
} | ||
} | ||
|
@@ -2536,7 +2536,7 @@ Describe "Mocking functions with conflicting parameters" { | |
$ParamToAvoid | ||
} | ||
|
||
Mock Get-ExampleTest { "World" } -ParameterFilter { $_ParamToAvoid -eq "Hello" } | ||
Mock Get-ExampleTest { "World" } -ParameterFilter { $_ParamToAvoid -eq "Hello" } -AllowFallback | ||
} | ||
|
||
It 'executes the mock' { | ||
|
@@ -2798,6 +2798,79 @@ Describe 'Mocking using ParameterFilter' { | |
} | ||
} | ||
|
||
Describe 'Calls not matching ParameterFilter' { | ||
BeforeAll { | ||
function demo ($name) { | ||
"hello $name" | ||
} | ||
} | ||
|
||
Context 'When default mock does not exist in parent scope' { | ||
It 'Throws by default' { | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } | ||
{ demo -name 'you' } | Should -Throw | ||
} | ||
|
||
It 'Calls locally defined default mock' { | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } | ||
Mock demo { 'default mock' } | ||
demo -name 'you' | Should -Be 'default mock' | ||
} | ||
|
||
It 'Calls real function when -AllowFallback is used' { | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } -AllowFallback | ||
demo -name 'you' | Should -Be 'hello you' | ||
} | ||
|
||
It 'Calls real function when at least one parameterized mock has -AllowFallback' { | ||
# TODO: Is this expected? Or should all parameterized mocks allow fallback? | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } -AllowFallback | ||
Mock demo -ParameterFilter { $name -eq 'Wisconsin' } -MockWith { 'mocked2' } | ||
demo -name 'you' | Should -Be 'hello you' | ||
} | ||
Comment on lines
+2825
to
+2830
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How should this work? This is where |
||
} | ||
|
||
Context 'When default mock exists in parent scope' { | ||
BeforeAll { | ||
Mock demo -MockWith { 'default mock' } | ||
} | ||
It 'Calls default mock without -AllowFallback' { | ||
# TODO: Is this expected? Should -AllowFallback only have effect when there's no default mock at any level? | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } | ||
demo -name 'you' | Should -Be 'default mock' | ||
} | ||
Comment on lines
+2837
to
+2841
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is likely a common use case where a command returns a default result while some parameterized mocks returns special output. Good as is or any exceptions to this behavior? |
||
} | ||
|
||
Context 'When mock with ParameterFilter exists in parent scope' { | ||
BeforeAll { | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } | ||
} | ||
It 'Throws by default' { | ||
{ demo -name 'you' } | Should -Throw | ||
} | ||
|
||
It 'Calls locally defined default mock' { | ||
Mock demo { 'default mock' } | ||
demo -name 'you' | Should -Be 'default mock' | ||
} | ||
} | ||
|
||
Context 'When mock with ParameterFilter and -AllowFallback exists in parent scope' { | ||
BeforeAll { | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } -AllowFallback | ||
} | ||
It 'Calls real function' { | ||
demo -name 'you' | Should -Be 'hello you' | ||
} | ||
|
||
It 'Throws when a more local parameterized mock does not allow fallback' -Skip { | ||
# TODO:Do we expect this? If so, we need to expose mock scope depth from Get-AllMockBehaviors | ||
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } | ||
{ demo -name 'you' } | Should -Throw | ||
Comment on lines
+2866
to
+2869
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How should this scenario work? Should a local parameterized mock be able to block fallback to original command when a parent mock allows it? |
||
} | ||
} | ||
} | ||
|
||
|
||
if ($PSVersionTable.PSVersion.Major -ge 3) { | ||
Describe "-RemoveParameterType" { | ||
|
Check warning
Code scanning / PSScriptAnalyzer
The parameter 'RemoveParameterValidation' has been declared but not used. Warning