Created
February 11, 2018 02:18
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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