Skip to content

Bring a function out of the module instead of moving the test into it #2998

Description

@nohwnd

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.

🤖

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions