Can somebody explain the correct way to mock a 3rd party module function from another module #2070
-
Ok, I've been having an issue with mocks particularly when mutiple modules are involved. I initially intended to post this question to get some help and during collation of information and experimenting with debug settings, I finally managed to get my mock to be called as required. I wanted to preserve this info and actually thought it may be usefull to other developers if I posted this discussion ... Lets say we have a module A which depends on B. There is a public function A.do-action which calls a function B.do-task. What is the correct way to write the test, using the correct InModuleScope, Mock statements. The documentation only talks about mocking a function defined in the same module; ie A.do-action invoking A.do-task. So this statement for example: Context 'given: <UndoRename>, <EliziumPath>' {
It 'should: ' -Tag 'Current' -TestCases @(
@{ UndoRename = $true; EliziumPath = $(Join-Path -Path 'app' -ChildPath 'data'); }
) {
[hashtable]$parameters = @{ UndoRename = $UndoRename; EliziumPath = $EliziumPath; };
InModuleScope -ModuleName Elizium.Loopz -Parameters $parameters {
Mock -ModuleName Elizium.Krayola Get-EnvironmentVariable {
param(
[Parameter()]
[string]$Variable
)
Write-Host "enter !!! Get-EnvironmentVariable MOCK!, Variable: '$Variable'";
... confuses me as to why the mock does not get called. I am expecting this to mean, Elizium.Loopz is the module under test. It contains a function that will make a call out to a 3rd party function Get-EnvironmentVariable defined in module Elizium.Krayola. I noticed this statement in the module docn: Notice that in this example test script, all calls to Mock and Should -Invoke have had the -ModuleName MyModule parameter added. This tells Pester to inject the mock into the module's scope, which causes any calls to those commands from inside the module to execute the mock instead. This is the reason why I left in the module specifier (Elizium.Krayola) in the Mock statement. I discoverd that I could enable logging to see whats happening with the state of the mock and this is what I saw in the output:
... and when I debug this, this statement comes out after the Mock statement and before invoking the command under test which calls Get-EnvironmentVariable. If I change the Mock statement to:
ie, we change the module specifier to reflect where the invoke is coming from, not where the mocked command is defined, we get a different result. I am not going show the debug result as it is verbose, but it is interesting to note that a key difference in the output from the previous run is this line:
as opposed to what we saw previously as:
OLD: However, this is not working as I expect. The mock is never called, I do not see that Debug statement (when $DebugPreference set to Continue). EDIT: With this change, the mock now gets called, even though the test now fails but for an entirely separate issue. So the spepcific part I am confused about is the module specifier in the InModuleScope and Mock statements. So which module do they refer to? The module implementing the command being mocked out or the module that peforms the invoke. So the lesson I gleaned from this is that the module specifier in the Mock statement should specify the module under test not the module where the mocked function is defined. This was not clear to me before as my previous tests were not of this type where you have a command being invoked which needs to be mocked out being defined in a 3rd party module. I hope this is helpful to other users. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
The docs show mocking/overriding an external command as part of the example code. # Just for giggles, we'll also mock Write-Host here, to demonstrate that you can
# mock calls to commands other than functions defined within the same module.
Mock -ModuleName MyModule Write-Host {} -Verifiable -ParameterFilter {
$Object -eq 'a build was run for version: 1.2'
} The docs for
Not the first or last time this parameter will be causing confusion, but this is a complicated topic to explain tbh. All help and PRs to the pester/docs-repo are appreciated 🙂 There's also a recent issue #2035 about the Here's a demo to illustrate the complexity of mocking things in modules. Maybe it helps or maybe... 🤯 😄 BeforeAll {
Get-Module one, two | Remove-Module
New-Module -Name one -ScriptBlock {
$secretvariable = "Module One is cool"
function coolFunc () {
"real"
$secretvariable
}
function OneCallsFunc() {
coolFunc
}
} | Import-Module
New-Module -Name two -ScriptBlock {
function TwoCallsFunc () {
"SomeOutput"
coolFunc
}
} | Import-Module
}
Describe 'Mocking in modules' {
BeforeAll {
$secretvariable = "Script scope 4 life"
}
It 'Original func' {
coolFunc | Should -Be 'real', "Module One is cool"
TwoCallsFunc | Should -Be 'SomeOutput', 'real', "Module One is cool"
}
Context 'Mock scriptblocks created in script' {
It 'Mock available in script' {
# Mock is created in script scope and publishes for current scope (test/script scope) since -ModuleName is not used
Mock -CommandName 'coolFunc' -MockWith {
"fake"
$secretvariable
}
# mock is available from script-scope and the secretvariable is read from script-scope
coolFunc | Should -Be 'fake', "Script scope 4 life"
# coolFunc called from inside Module two isn't aware of mock. Calls original coolFunc function as before
TwoCallsFunc | Should -Be 'SomeOutput', 'real', "Module One is cool"
}
It 'Mock available in module' {
# Mock is created in script-scope and published in Module two only
Mock -CommandName 'coolFunc' -MockWith {
"fake"
$secretvariable
} -ModuleName 'two'
# mock is not available in script-scope. original called
coolFunc | Should -Be 'real', "Module One is cool"
# coolFunc called from inside Module two triggers the mock. The mock was created in script-scope, so the secretvariable from script is used
TwoCallsFunc | Should -Be 'SomeOutput', 'fake', "Script scope 4 life"
}
}
Context 'Advanced - Mock scriptblocks created in modules' {
It 'Mock created in a module and available in that module only' {
# Mock is created in Module One and published in same scope (module one due Mock-command used inside InModuleScope)
InModuleScope -ModuleName 'one' {
Mock -CommandName 'coolFunc' -MockWith {
"fake"
$secretvariable
}
}
# mock is not available in script-scope. original called
coolFunc | Should -Be 'real', "Module One is cool"
# coolFunc called from inside Module two can't see the mock. original called
TwoCallsFunc | Should -Be 'SomeOutput', 'real', "Module One is cool"
# coolFunc called inside module one where the mock is found. mock was created in Module One, so secret value from module is used
OneCallsFunc | Should -Be 'fake', "Module One is cool"
}
It 'Mock created in a module but available in another module' {
# Mock is created in Module One and published in Module two only
InModuleScope -ModuleName 'one' {
Mock -CommandName 'coolFunc' -MockWith {
"fake"
$secretvariable
} -ModuleName 'two'
}
# mock is not available in script-scope. original called
coolFunc | Should -Be 'real', "Module One is cool"
# coolFunc called from inside Module two triggers the mock. The mock was created in Module One, so the secretvariable from Module one is used
TwoCallsFunc | Should -Be 'SomeOutput', 'fake', "Module One is cool"
}
It 'Mock created in a module but available in script-scope' {
# Mock is created in Module One and published in script scope.
# We can't wrap `Mock` in InModuleScope because we can't reference script-scope with `-ModuleName`. Need to only create the scriptblock inside module
$mockWithScriptBlock = InModuleScope -ModuleName 'one' {
# Output scriptblock bound to module
{
"fake"
$secretvariable
}
}
Mock -CommandName 'coolFunc' -MockWith $mockWithScriptBlock
# mock is available in script-scope. The mock (scriptblock) was created in Module One, so the secretvariable from Module one is used
coolFunc | Should -Be 'fake', "Module One is cool"
# coolFunc called from inside Module one can't find the mock as Mock -ModuleName 'one' wasn't used and Mock wasn't used inside InModuleScope. original called
OneCallsFunc | Should -Be 'real', "Module One is cool"
# coolFunc called from inside Module two can't find the mock as Mock -ModuleName 'two' wasn't used. original called
TwoCallsFunc | Should -Be 'SomeOutput', 'real', "Module One is cool"
}
}
} |
Beta Was this translation helpful? Give feedback.
The docs show mocking/overriding an external command as part of the example code.
The docs for
Mock
also tries to explain the meaning of ModuleName (same meaning in Mock and Should -Invoke).