Skip to content

Instantly share code, notes, and snippets.

@alx9r
Created February 11, 2018 02:18
Show Gist options
  • Save alx9r/ed9d837b83866ddae4b079700e935e66 to your computer and use it in GitHub Desktop.
Save alx9r/ed9d837b83866ddae4b079700e935e66 to your computer and use it in GitHub Desktop.
Proof-of-concept of the the shadowing required to safely mock a PowerShell function in a module.
Get-Module Pester,ModuleUnderTest | Remove-Module
$PSModuleAutoLoadingPreference = 'None'
<#
This is a proof-of-concept of the shadowing required
to safely mock a function in a module.
This is achieved by first creating the mock function in a
new, temporary scope in the module. (The mock function the
temporary scope merely "shadows" rather than overwrites the
original function.) Then, while that temporary scope is still
active for the module, invoke and complete the test that
relies on the mock from outside the module. Finally, exit
the temporary scope thereby disposing of the shadow and returning
the module to its original state.
This requires the suspension of the scriptblock comprising the
temporary scope while the test scriptblock is invoked and run
to completion. The control flow characteristics of PowerShell's
pipeline are used to achieve this.
#>
### start of library code
<#
This is entry point for the test fixture.
#>
function ShadowAndTest {
param
(
[Parameter(Mandatory)]
[psmoduleinfo]
$ShadowedModule,
[Parameter(Mandatory)]
[scriptblock]
$ShadowScript,
[Parameter(Mandatory)]
[scriptblock]
$Test
)
<#
The substatement operator is used here to ensure that Shadow
completes before the result of RunTest is output.
#>
$(
Shadow $ShadowedModule -ShadowScript $ShadowScript |
RunTest -Test $Test
)
}
<#
This is the first half of the fixture. Its job is to do the following:
1. Switch the session state to the module being shadowed.
2. Push a new temporary scope onto the module's scope stack.
3. Execute the scriptblock that creates the shadow in the new scope.
4. Yield to RunTest while keeping temporary scope active.
5. Pop the temporary scope from the module's scope stack, thereby
removing the shadow.
6. Switch the session state from the module being shadowed.
#>
function Shadow {
param
(
[Parameter(Position=1,Mandatory)]
[psmoduleinfo]
$ShadowedModule,
[Parameter(Mandatory)]
[scriptblock]
$ShadowScript
)
<#
An unbound copy of the scriptblock is created here
otherwise its $ShadowScript's SessionState will be
used instead of $ShadowedModule's.
#>
[scriptblock]::Create($ShadowScript) |
& $ShadowedModule {
# switch to $ShadowedModule's SessionState and create a new scope
process
{
. $_ # Invoke the scriptblock in this scope so the shadow
# survives past this line.
'Pawl' # This is the pawl to RunTest's Tooth. Together they
# pin this scope until RunTest completes.
}
# destroy this scope thereby destroying the shadow
# switch out of $ShadowedModule's SessionState
}
}
<#
This is the second half of the fixture. Its job is to do the following:
7. Wait for Shadow to yield.
8. Invoke the test scriptblock.
9. Output the results of the test scriptblock.
10. Resume execution in Shadow.
#>
function RunTest {
param
(
# This is the tooth to Shadows 'Pawl'. 'Pawl' is never used
# except to cause process{} to run at the right time.
[Parameter(Mandatory,ValueFromPipeline)]
$Tooth,
[Parameter(Mandatory)]
[scriptblock]
$Test
)
process
{
& $Test
}
}
### end of library code
### start of code that is standing in for user test code
<#
This module contains the function under test. When ShadowAndTest
is run, function g will be shadowed just long enough to invoke f.
#>
New-Module ModuleUnderTest {
function f {
"real f - $(g)"
}
function g {
'real g'
}
} | Import-Module
<#
At this point ModuleUnderTest has not been subject to anything
unusual, so the real function g is invoked.
#>
f # real f - real g
$splat = @{
ShadowedModule = (Get-Module ModuleUnderTest) # a reference to the module under test
ShadowScript = { function g {'fake g'} } # the scriptblock that creates the mock
Test = { f } # the scriptblock that runs the test
}
ShadowAndTest @splat # real f - fake g
<#
By this point ModuleUnderTest's SessionState has been altered.
The output of this call confirms that the alteration was correctly undone
#>
f # real f - real g
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment