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

Fix mock behavior for Mock and Should -Invoke #1915

Merged
merged 11 commits into from
Apr 23, 2021
Merged
2 changes: 1 addition & 1 deletion src/Main.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ function Invoke-Pester {
# TODO: Remove all references to mock table, there should not be many.
$script:mockTable = @{}
# todo: move mock cleanup to BeforeAllBlockContainer when there is any
Remove-MockFunctionsAndAliases
Remove-MockFunctionsAndAliases -SessionState $PSCmdlet.SessionState
}

end {
Expand Down
83 changes: 59 additions & 24 deletions src/functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ function New-MockBehavior {

[PSCustomObject] @{
CommandName = $ContextInfo.Command.Name
ModuleName = if ($ContextInfo.IsFromRequestedModule) { $ContextInfo.Module.Name } else { $null }
ModuleName = $ContextInfo.TargetModule
Filter = $ParameterFilter
IsDefault = $null -eq $ParameterFilter
IsInModule = -not [string]::IsNullOrEmpty($ContextInfo.TargetModule)
Verifiable = $Verifiable
ScriptBlock = $MockWith
Hook = $Hook
Expand All @@ -54,7 +55,7 @@ function EscapeSingleQuotedStringContent ($Content) {

function Create-MockHook ($contextInfo, $InvokeMockCallback) {
$commandName = $contextInfo.Command.Name
$moduleName = if ($contextInfo.IsFromRequestedModule) { $contextInfo.Module.Name } else { '' }
$moduleName = $contextInfo.TargetModule
$metadata = $contextInfo.CommandMetadata
$cmdletBinding = ''
$paramBlock = ''
Expand Down Expand Up @@ -198,17 +199,19 @@ function Create-MockHook ($contextInfo, $InvokeMockCallback) {

$mockScript = [scriptblock]::Create($code)

$mockName = "PesterMock_$(if ([string]::IsNullOrEmpty($ModuleName)) { "<none>" } else { $ModuleName })_$CommandName_$([Guid]::NewGuid().Guid)"

$mock = @{
OriginalCommand = $contextInfo.Command
OriginalMetadata = $contextInfo.CommandMetadata
OriginalMetadata2 = $contextInfo.CommandMetadata2
OriginalMetadata2 = $contextInfo.CommandMetadata2
CommandName = $commandName
SessionState = $contextInfo.SessionState
CallerSessionState = $contextInfo.CallerSessionState
Metadata = $metadata
DynamicParamScriptBlock = $dynamicParamScriptBlock
Aliases = [Collections.Generic.List[object]]@($commandName)
BootstrapFunctionName = 'PesterMock_' + [Guid]::NewGuid().Guid
BootstrapFunctionName = $mockName
}

if ($mock.OriginalCommand.ModuleName) {
Expand Down Expand Up @@ -367,7 +370,7 @@ function Should-InvokeInternal {
$ModuleName = $SessionState.Module.Name
}

$ModuleName = if ($ContextInfo.IsFromRequestedModule) { $ContextInfo.Module.Name } else { $null }
$ModuleName = $ContextInfo.TargetModule
$CommandName = $ContextInfo.Command.Name

$callHistory = $MockTable["$ModuleName||$CommandName"]
Expand Down Expand Up @@ -420,7 +423,8 @@ function Should-InvokeInternal {
FailureMessage = "Expected ${commandName}${moduleMessage} to be called less than $Times times but was called $($matchingCalls.Count) times"
}
}
} else {
}
else {
if ($matchingCalls.Count -ne $Times -and ($Exactly -or ($Times -eq 0))) {
return [PSCustomObject] @{
Succeeded = $false
Expand Down Expand Up @@ -555,6 +559,7 @@ function Resolve-Command {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Resolving command $CommandName."
}

if ($ModuleName) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "ModuleName was specified searching for the command in module $ModuleName."
Expand Down Expand Up @@ -584,25 +589,24 @@ function Resolve-Command {
}
}
}

if (-not $command) {


# TODO: this resolves the command in the caller scope if the command was not found in the module scope, but that does not make sense does it? When the user specifies that he want's to use Module it should use just Module. Disabling the fall through makes tests fail.
else {
# we used to fallback to the script scope when command was not found in the module, we no longer do that
# now we just search the script scope when module name is not specified. This was probably needed because of
# some incosistencies of resolving the mocks. But it never made sense to me.

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Searching for command $CommandName in the caller scope."
Write-PesterDebugMessage -Scope Mock "Searching for command $CommandName in the script scope."
}
Set-ScriptBlockScope -ScriptBlock $findAndResolveCommand -SessionState $SessionState
$command, $commandMetadata, $commandMetadata2 = & $findAndResolveCommand -Name $CommandName
if ($command) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Found the command $CommandName in the caller scope$(if ($CommandName -ne $command.Name) {" and it resolved to $($command.Name)"})."
Write-PesterDebugMessage -Scope Mock "Found the command $CommandName in the script scope$(if ($CommandName -ne $command.Name) {" and it resolved to $($command.Name)"})."
}
}
else {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Did not find command $CommandName in the caller scope."
Write-PesterDebugMessage -Scope Mock "Did not find command $CommandName in the script scope."
}
}
}
Expand All @@ -621,7 +625,7 @@ function Resolve-Command {
return @{
Command = $command.Mock.Hook.OriginalCommand
CommandMetadata = $command.Mock.Hook.OriginalMetadata
CommandMetadata2 = $command.Mock.Hook.OriginalMetadata2
CommandMetadata2 = $command.Mock.Hook.OriginalMetadata2
# the session state of the target module
SessionState = $command.Mock.Hook.SessionState
# the session state in which we invoke the mock body (where the test runs)
Expand All @@ -630,8 +634,9 @@ function Resolve-Command {
Module = $command.Mock.Hook.OriginalCommand.Module
# true if we inserted the mock into a module
IsFromModule = $null -ne $module
TargetModule = $ModuleName
# true if the commmand comes from the target module
IsFromRequestedModule = $null -ne $module -and $ModuleName -eq $command.Mock.Hook.OriginalCommand.Module.Name
IsFromTargetModule = $null -ne $module -and $ModuleName -eq $command.Mock.Hook.OriginalCommand.Module.Name
IsMockBootstrapFunction = $true
Hook = $command.Mock.Hook
}
Expand All @@ -647,7 +652,11 @@ function Resolve-Command {
Module = $module

IsFromModule = $null -ne $module
IsFromRequestedModule = $null -ne $module -and $module.Name -eq $ModuleName
# The target module in which we are inserting the mock, this may not be the same as the module in which the
# function is defined. For example when module m exports function f, and we mock it in script scope or in module o.
# They would be the same if we mock an internal function in module m by specifying -ModuleName m, to be able to test it.
TargetModule = $ModuleName
IsFromTargetModule = $null -ne $module -and $module.Name -eq $ModuleName
IsMockBootstrapFunction = $false
Hook = $null
}
Expand Down Expand Up @@ -872,7 +881,7 @@ function FindMatchingBehavior {
)

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Finding a mock behavior."
Write-PesterDebugMessage -Scope Mock "Finding behavior to use, one that passes filter or a default:"
}

$foundDefaultBehavior = $false
Expand Down Expand Up @@ -1108,7 +1117,7 @@ function Test-ParameterFilter {

if ($PesterPreference.Debug.WriteDebugMessages.Value) {
$hasContext = 0 -lt $Context.Count
$c = $(if ($hasContext) {foreach ($p in $Context.GetEnumerator()) { "$($p.Key) = $($p.Value)" }}) -join ", "
$c = $(if ($hasContext) { foreach ($p in $Context.GetEnumerator()) { "$($p.Key) = $($p.Value)" } }) -join ", "
Write-PesterDebugMessage -Scope Mock -Message "Running mock filter { $scriptBlock } $(if ($hasContext) { "with context: $c" } else { "without any context"})."
}

Expand Down Expand Up @@ -1504,20 +1513,46 @@ function Test-IsClosure {
)
}

function Remove-MockFunctionsAndAliases {
function Remove-MockFunctionsAndAliases ($SessionState) {
# when a test is terminated (e.g. by stopping at a breakpoint and then stoping the execution of the script)
# the aliases and bootstrap functions for the currently mocked functions will remain in place
# Then on subsequent runs the bootstrap function will be picked up instead of the real command,
# because there is still an alias associated with it, and the test will fail.
# So before putting Pester state in place we should make sure that all Pester mocks are gone
# by deleting every alias pointing to a function that starts with PesterMock_. Then we also delete the
# bootstrap function.
foreach ($alias in (& $script:SafeCommands['Get-Alias'] -Definition "PesterMock_*")) {
& $script:SafeCommands['Remove-Item'] "alias:/$($alias.Name)"
$Get_Alias = $script:SafeCommands['Get-Alias']
$Get_Command = $script:SafeCommands['Get-Command']
$Remove_Item = $script:SafeCommands['Remove-Item']
foreach ($alias in (& $Get_Alias -Definition "PesterMock_*")) {
& $Remove_Item "alias:/$($alias.Name)"
}

foreach ($bootstrapFunction in (& $script:SafeCommands['Get-Command'] -Name "PesterMock_*")) {
& $script:SafeCommands['Remove-Item'] "function:/$($bootstrapFunction.Name)"
foreach ($bootstrapFunction in (& $Get_Command -Name "PesterMock_*")) {
& $Remove_Item "function:/$($bootstrapFunction.Name)"
}

$ScriptBlock = {
param ($Get_Alias, $Get_Command, $Remove_Item)
foreach ($alias in (& $Get_Alias -Definition "PesterMock_*")) {
& $Remove_Item "alias:/$($alias.Name)"
}

foreach ($bootstrapFunction in (& $Get_Command -Name "PesterMock_*")) {
& $Remove_Item "function:/$($bootstrapFunction.Name)"
}
}

# clean up in caller session state
Set-ScriptBlockScope -SessionState $SessionState -ScriptBlock $ScriptBlock
& $ScriptBlock $Get_Alias $Get_Command $Remove_Item

# clean up also in all loaded script modules
$modules = & $script:SafeCommands['Get-Module']
foreach ($module in $modules) {
if ('Script' -eq $module.ModuleType) {
& ($module) $ScriptBlock $Get_Alias $Get_Command $Remove_Item
}
}
}

Expand Down
Loading