InModuleScope moves the test into the module. That is backwards, what I want is the code under test to come out.
The inversion is where the -Parameters problem comes from. Your BeforeAll variables are gone because the test is no longer standing where it was written. It is also all or nothing, a whole It or Describe goes inside, and the test silently gets access to every internal it never meant to touch.
Taking the function out works, and it needs no proxy:
$cmd = & (Get-Module MyModule) { Get-Command Format-Row }
${function:BroughtOut} = $cmd.ScriptBlock
The SessionStateInternal survives the assignment. I checked it by reflection and it is the same object, so the body still runs in module scope, $script: variables and other internal functions resolve, while the call site stays in the test scope.
Parameter fidelity comes for free, ValidateSet is enforced, Mandatory, defaults, and the CmdletBinding common parameters are all there. No metadata regeneration, no generated proxy.
Why this needs #2997 to be useful
Extraction solves calling an internal. It does nothing for mocking. Format-Row calls Get-Config and that mock still has to land in module scope.
The two together are what InModuleScope is doing for people today. That is why they reach for it, it is the one construct that gives both at once.
| job |
solved by |
| calling an internal function |
this |
| mocking inside the module |
#2997 |
seeding or asserting module $script: state |
neither, see below |
What stays
Module state, and classes.
& $module { $script:Cache = 'x' } covers the state case with the same mechanism. Classes defined in a module stay hard to reach from outside and I do not have an answer for those.
So InModuleScope does not go away. It goes from being the normal way to test internals, to being the escape hatch for state and classes. That is a smaller and more honest thing to document.
Shape
BeforeAll {
Import-Module $PSScriptRoot/../src/MyModule -Force
# bring Format-Row out
}
It 'upper-cases the row' {
Mock Get-Config { @{ Case = 'Upper' } } # lands in MyModule, see #2997
Format-Row 'a' | Should -Be 'A' # internal, called from test scope
}
Whether the name is injected into the test scope, which reads better but can collide, or handed back as a command object, which is collision free but reads worse, I do not have a strong opinion. Probably both, with the object as the primitive.
Part of #2996.
🤖
InModuleScopemoves the test into the module. That is backwards, what I want is the code under test to come out.The inversion is where the
-Parametersproblem comes from. YourBeforeAllvariables are gone because the test is no longer standing where it was written. It is also all or nothing, a wholeItorDescribegoes inside, and the test silently gets access to every internal it never meant to touch.Taking the function out works, and it needs no proxy:
The
SessionStateInternalsurvives the assignment. I checked it by reflection and it is the same object, so the body still runs in module scope,$script:variables and other internal functions resolve, while the call site stays in the test scope.Parameter fidelity comes for free,
ValidateSetis enforced,Mandatory, defaults, and theCmdletBindingcommon parameters are all there. No metadata regeneration, no generated proxy.Why this needs #2997 to be useful
Extraction solves calling an internal. It does nothing for mocking.
Format-RowcallsGet-Configand that mock still has to land in module scope.The two together are what
InModuleScopeis doing for people today. That is why they reach for it, it is the one construct that gives both at once.$script:stateWhat stays
Module state, and classes.
& $module { $script:Cache = 'x' }covers the state case with the same mechanism. Classes defined in a module stay hard to reach from outside and I do not have an answer for those.So
InModuleScopedoes not go away. It goes from being the normal way to test internals, to being the escape hatch for state and classes. That is a smaller and more honest thing to document.Shape
BeforeAll { Import-Module $PSScriptRoot/../src/MyModule -Force # bring Format-Row out } It 'upper-cases the row' { Mock Get-Config { @{ Case = 'Upper' } } # lands in MyModule, see #2997 Format-Row 'a' | Should -Be 'A' # internal, called from test scope }Whether the name is injected into the test scope, which reads better but can collide, or handed back as a command object, which is collision free but reads worse, I do not have a strong opinion. Probably both, with the object as the primitive.
Part of #2996.
🤖