Skip to content
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
82 changes: 47 additions & 35 deletions src/functions/Pester.SessionState.Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -366,51 +366,63 @@ function Get-VerifiableBehaviors {
$currentTest = Get-CurrentTest
$inTest = $null -ne $currentTest

$behaviors = [System.Collections.Generic.List[Object]]@()
# Collect the mock-behavior tables from the closest scope (the test) outwards to the root block, so we can
# walk them in precedence order. A default mock (one without a -ParameterFilter) in a closer scope shadows
# every behavior for the same command in an outer scope, because the closer default catches all calls and
# the outer behaviors can never be invoked. A verifiable behavior that is shadowed like that must not be
# required by Should -InvokeVerifiable, otherwise overriding a verifiable mock in an inner scope always
# fails verification. (#2672)
$behaviorTables = [System.Collections.Generic.List[Object]]@()
if ($inTest) {
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "We are in a test. Finding all behaviors in this test."
}
$allBehaviors = $currentTest.PluginData.Mock.Behaviors.Values
if ($null -ne $allBehaviors -and $allBehaviors.Count -gt 0) {
# all behaviors for all commands
foreach ($commandBehaviors in $allBehaviors) {
if ($null -ne $commandBehaviors -and $commandBehaviors.Count -gt 0) {
# all behaviors for single command
foreach ($behavior in $commandBehaviors) {
if ($behavior.Verifiable) {
$behaviors.Add($behavior)
}
}
}
}
}
$behaviorTables.Add($currentTest.PluginData.Mock.Behaviors)
}
$block = Get-CurrentBlock

# recurse up
while ($null -ne $block) {
$behaviorTables.Add($block.PluginData.Mock.Behaviors)
$block = $block.Parent
}

## action
$allBehaviors = $block.PluginData.Mock.Behaviors.Values
# all behaviors for all commands
if ($null -ne $allBehaviors -or $allBehaviors.Count -ne 0) {
foreach ($commandBehaviors in $allBehaviors) {
if ($null -ne $commandBehaviors -and $commandBehaviors.Count -gt 0) {
# all behaviors for single command
foreach ($behavior in $commandBehaviors) {
if ($behavior.Verifiable) {
$behaviors.Add($behavior)
}
}
$behaviors = [System.Collections.Generic.List[Object]]@()
$shadowedCommands = @{}

foreach ($behaviorTable in $behaviorTables) {
if ($null -eq $behaviorTable) {
continue
}

$allBehaviors = $behaviorTable.Values
if ($null -eq $allBehaviors -or $allBehaviors.Count -eq 0) {
continue
}

# Commands that get a default mock in this scope shadow the same command in outer scopes. Gather them
# and only apply the shadowing after this scope is processed, so behaviors defined together in the same
# scope (e.g. a default and a parameter-filtered mock) don't shadow each other.
$defaultedInThisScope = [System.Collections.Generic.List[string]]@()

foreach ($commandBehaviors in $allBehaviors) {
if ($null -eq $commandBehaviors -or $commandBehaviors.Count -eq 0) {
continue
}

$key = "$($commandBehaviors[0].ModuleName)||$($commandBehaviors[0].CommandName)"
$isShadowed = $shadowedCommands.ContainsKey($key)

foreach ($behavior in $commandBehaviors) {
if ($behavior.Verifiable -and -not $isShadowed) {
$behaviors.Add($behavior)
}

if ($behavior.IsDefault) {
$defaultedInThisScope.Add($key)
}
}
}

# end action
$block = $block.Parent
foreach ($shadowedKey in $defaultedInThisScope) {
$shadowedCommands[$shadowedKey] = $true
}
}
# end

$behaviors
}
Expand Down
64 changes: 64 additions & 0 deletions tst/Pester.Mock.RSpec.ts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,70 @@ i -PassThru:$PassThru {
$t = $r.Containers[0].Blocks[0].Tests[0]
$t.Result | Verify-Equal "Passed"
}

t "verifiable mock overridden by a verifiable mock in a narrower scope passes (#2672)" {
$sb = {
Describe 'd' {
BeforeAll { function Get-Data { 'real' } }
Context 'c' {
BeforeAll { Mock Get-Data { 'outer' } -Verifiable }
It 'i' {
Mock Get-Data { 'inner' } -Verifiable
Get-Data | Should -Be 'inner'
Should -InvokeVerifiable
}
}
}
}

$r = Invoke-Pester -Configuration ([PesterConfiguration]@{
Run = @{ ScriptBlock = $sb; PassThru = $true }
})

$r.Containers[0].Blocks[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
}

t "verifiable mock overridden by a default mock in a narrower scope is not required (#2672)" {
$sb = {
Describe 'd' {
BeforeAll { function Get-Data { 'real' } }
Context 'c' {
BeforeAll { Mock Get-Data { 'outer' } -Verifiable }
It 'i' {
# not verifiable, but a default mock so it shadows the outer verifiable one
Mock Get-Data { 'inner' }
Get-Data | Should -Be 'inner'
Should -InvokeVerifiable
}
}
}
}

$r = Invoke-Pester -Configuration ([PesterConfiguration]@{
Run = @{ ScriptBlock = $sb; PassThru = $true }
})

$r.Containers[0].Blocks[0].Blocks[0].Tests[0].Result | Verify-Equal "Passed"
}

t "an uninvoked verifiable mock that is not shadowed still fails InvokeVerifiable" {
$sb = {
Describe 'd' {
BeforeAll { function Get-Data { 'real' } }
It 'i' {
Mock Get-Data { 'mock' } -Verifiable
# never call Get-Data, so the verifiable mock must make InvokeVerifiable fail
Should -InvokeVerifiable
}
}
}

$r = Invoke-Pester -Configuration ([PesterConfiguration]@{
Run = @{ ScriptBlock = $sb; PassThru = $true }
})

$r.Containers[0].Blocks[0].Tests[0].Result | Verify-Equal "Failed"
}
}

b "top-level mocks" {
Expand Down
Loading