-
-
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?
Conversation
@@ -28,20 +28,22 @@ | |||
[Parameter(Mandatory)] | |||
$Hook, | |||
[string[]]$RemoveParameterType, | |||
[string[]]$RemoveParameterValidation | |||
[string[]]$RemoveParameterValidation, |
Check warning
Code scanning / PSScriptAnalyzer
The parameter 'RemoveParameterValidation' has been declared but not used. Warning
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nohwnd Experimented a little with this. Not sure if this is the most intuitive direction. See comments. What do you think?
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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?
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' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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?
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' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should this work? This is where -AllowFallback/AllowOriginal/AllowPassthrough
gets ugly compared to an explicit Mock demo -Throw
shortcut to implement a guard mock.
CommandName = $ContextInfo.Command.Name | ||
ModuleName = $ContextInfo.TargetModule | ||
Filter = $ParameterFilter | ||
AllowFallback = $AllowFallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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". AllowOriginal
could give expectation of blocking stuff like & (Get-Command Get-ChildItem -CommandType Cmdlet)
. AllowPassthrough
is unclear. Other short suggestions?
Focus on other review comments first in case this design is discarded.
[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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to support all original positional parameters or is CommandName
+ MockWith
enough? Wouldn't expect the rest to be commonly used.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably update text and/or use separate line.
I don't expect the original behavior to be useful to almost anyone, I would add option to configuration for the whole testbase and that is it. If you really need the fallback only in a specific case you can still add a default mock that calls the original command via & (Get-Command ...). At least that is my current view, and imho the easiest way to implement this change, and we can add the disabling on a different levels if we get enough good reasons to do that. |
Oh... 😅 I assumed it was common to e.g. mock |
Not sure I'm following what you are discussing, but if it is relevant here is my 2c... I would say it is pretty normala to write a |
Yes I agree, this would just add another layer of complexity to an already hard thing to learn for folks and for me as a reviewer to grasp when reading code. |
This is what I'm looking for: function Get-SomeFile {
$file = Get-Item -Path 'myFile.txt'
Get-Content -Path $file.FullName -Raw
}
Describe 'Something' {
BeforeAll {
# Guard mock
Mock Get-Item -Throw
}
Context 'When something happens' {
It 'Should do something' {
# FAILS: This fails because Get-Item is not mocked
{ Get-SomeFile } | Should-Be 'myFile content'
}
}
Context 'When something else happens' {
BeforeAll {
Mock Get-Item -MockWith { @{ FullName = 'myFile.txt' } }
# Guard mock 2
Mock Get-Content -Throw
}
It 'Should do something' {
Mock Get-Content -MockWith { 'myFile content' }
# PASS: This passes because mocks are set up
{ Get-SomeFile } | Should-Be 'myFile content'
}
}
} |
Okay, thanks for checking my assumptions. I for myself never used the fall through to real command, and only occasionally I've called the real command from a mock when calling Write-Host or similar "utility" command. In this case I don't know what to do exactly, putting the allow parameter on all mocks in scope is complicated, and defining a default mock, just to let it call the real command sounds counterintuitive. |
That's the issue with trying to solve two separate issues with one PR 😄 # MockHelpers.ps1
function RequireMock {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $CommandName,
[ScriptBlock] $ParameterFilter,
[string] $ModuleName
)
$mockScriptBlock = {
throw "Missing mock for '$CommandName'."
}.GetNewClosure()
Pester\Mock @PSBoundParameters -MockWith $mockScriptBlock
}
# Demo.tests.ps1
BeforeAll {
. "$PSScriptRoot/MockHelpers.ps1"
}
Describe 'BigScriptWithALotOfMocks' {
BeforeAll {
RequireMock -CommandName Get-Something1
RequireMock -CommandName Get-Something2
}
Context 'When testing scenario 1' {
BeforeAll {
Mock -CommandName Get-Something1
Mock -CommandName Get-Something2
}
It 'Should return $true' {
BigScriptWithALotOfMocks | Should -BeTrue
}
}
} The other request wanted to make this implicit when any mock exists for the command. Implicit behavior would be a global setting, but IMO we'd also need a override doing the opposite as your design, which is a little more messy with multiple mocks. E.g. It 'Who wins? Do we allow original command or not?' {
Mock demo -ParameterFilter { $name -eq 'world' } -MockWith { 'mocked' } -AllowFallback
Mock demo -ParameterFilter { $name -eq 'Wisconsin' } -MockWith { 'mocked2' }
demo -name 'you' | Should -Be 'hello you'
}
It 'Or maybe a separate parameter-set to enable it like a flag at that scope?' {
Mock demo -AllowFallback
Mock demo -ParameterFilter { $name -eq 'Wisconsin' } -MockWith { 'mocked2' }
demo -name 'you' | Should -Be 'hello you'
}
It 'Or just a shortcut syntax to re-enable passthrough' {
Mock demo -InvokeOriginal # Default mock will passthrough
Mock demo -InvokeOriginal -ParameterFilter { $name -eq 'world' } # Passthrough
Mock demo -ParameterFilter { $name -eq 'Wisconsin' } -MockWith { 'mocked2' }
demo -name 'you' | Should -Be 'hello you'
} I see the value of both though I'd probably wouldn't use either often. My suggestions:
Alternative 2 - Require explicit code |
I think I'm following now, you trying to handle two scenarios where the new functionality is either implicitly enabled or disabled.
If the above is alternative 1 then I vote for alternative 1. 🙂 If alternativ 2 demands users to write even complex mocks to be able to call original command then I suggest we avoid that alternative. But would be okay for |
Correct
My alt. 2 was to not change the current behavior. So you'd only write a mock to call the original command if there's already a manual guard mock defined, e.g. in a root
Yeah, publishing it as a module requires a little session state trickery to support all scenarios. |
The main reason for adding a guard mock in a test I made (and the reason for issue #2166) was so an initial author of a test (me in this example) in the Describe/BeforeAll-block can say "This command we call must always be mocked in all context-blocks, otherwise bad things will happen to the dev machine". Other contributors will not end up in the pit because of the guard mock. For that reason the If we in Pester 6 simplify this PR to:
This would be a breaking change with Pester 6 for existing tests unless it possible to disable it. Then this would also hopefully still be allow to create a default (guard) mock: function MySuperCommand { Do-PreSetup; Do-DevastatingWork }
Describe {
BeforeAll {
Mock -CommandName Do-DevastatingWork -MockWith { throw 'This must be mocked' }
}
Context 'Test pre setup' {
It 'should call Do-PreSetup' {
Mock -CommandName Do-PreSetup
Mock -CommandName Do-DevastatingWork -MockWith { '1' }
MySuperCommand
}
}
Context 'Test pre setup' {
It 'should call Do-PreSetup' {
Mock -CommandName Do-PreSetup
Mock -CommandName Do-DevastatingWork -MockWith { '2' }
MySuperCommand
}
}
} |
PR Summary
WIP
When a command is mocked by only parameterized mocks, throw for calls not matching any filters unless
-AllowFallback
is specified.Fix #2166
Fix #2178
TODO
-AllowFallback
could reference fallback to default mock in module or script-scope-AllowFallback
is weird with multiple parameterized mocks in scope. Better to introduceMock -Throw
shortcut which can be used for both default mock and special parameterized tests?Mocking with nested Pester runs.Mocking works in nested run
PR Checklist
Create Pull Request
to mark it as a draft. PR can be markedReady for review
when it's ready.