Testing with InModuleScope

Hi everyone,

Just came across the article:

let me introduce you to the InModuleScope command. It allows you to inject some or all of your test code into a script module, which gives you direct access to all of its internal bits and pieces

So, I have a theoretical question about InModuleScope. Is using InModuleScope the correct way to test scripts from the scope perspective? Because I assume if you are creating a module and export some its members, they will be used from outside your module, i.e. from an external scope. But InModuleScope forces me to test module members (functions) inside the module scope which means I am not testing it in the way which it will be used. Hopefully you understand my question.

A bit of context of why I am asking: I am trying to mock ActiveDirectory objects. And to do that I am exporting them to a JSON file first and then feed it to Mock command, similar to this article. I understand I have to stick with InModuleScope, because -ModuleName parameter with Mock command just does not work properly because of different scopes.

Hi Evgeny

InModuleScope allow you to test internal functions in a module (functions that have not been exported).

Consider the following module:

$m = New-Module -Name FooBar -ScriptBlock {

    function Get-Foo {
        'FOO!'
    }

    function Get-Bar {
        'BAR!'
    }
    
    function Get-FooBar {
        '{0} {1}' -f (Get-Foo), (Get-Bar)
    }

    Export-ModuleMember -Function Get-Foo, Get-FooBar

}
$m | Import-Module -Force

When testing that module without InModuleScope, the Get-Bar function is not available for testing

Describe 'Function: Get-Foo' {

    It 'Should return expected text' {
    
        Get-Foo | Should Be 'FOO!'
    
    }

}

Describe 'Function: Get-FooBar' {

    It 'Should return expected text' {
    
        Get-FooBar | Should Be 'FOO! BAR!'
    
    }

}

Describe 'Function: Get-Bar' {

    It 'Should return expected text' {
    
        Get-Bar | Should Be 'BAR!'
    
    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 78ms

Describing Function: Get-FooBar
  [+] Should return expected text 51ms

Describing Function: Get-Bar
  [-] Should return expected text 153ms
    CommandNotFoundException: The term 'Get-Bar' is not recognized as the name of a cmdlet, function, script file, or 
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try a
gain.
    at , : line 26

Wrapping the tests in an InModuleScope block will allow you to test the internal Get-Bar function

InModuleScope $m.Name {

    Describe 'Function: Get-Foo' {

        It 'Should return expected text' {
    
            Get-Foo | Should Be 'FOO!'
    
        }

    }

    Describe 'Function: Get-FooBar' {

        It 'Should return expected text' {
    
            Get-FooBar | Should Be 'FOO! BAR!'
    
        }

    }

    Describe 'Function: Get-Bar' {

        It 'Should return expected text' {
    
            Get-Bar | Should Be 'BAR!'
    
        }

    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 449ms

Describing Function: Get-FooBar
  [+] Should return expected text 241ms

Describing Function: Get-Bar
  [+] Should return expected text 48ms

But instead of wrapping all of the tests in the InModuleScope block, you can limit it to only those needed

Describe 'Function: Get-Foo' {

    It 'Should return expected text' {
    
        Get-Foo | Should Be 'FOO!'
    
    }

}

Describe 'Function: Get-FooBar' {

    It 'Should return expected text' {
    
        Get-FooBar | Should Be 'FOO! BAR!'
    
    }

}

Describe 'Function: Get-Bar' {

    InModuleScope $m.Name {
    
        It 'Should return expected text' {
    
            Get-Bar | Should Be 'BAR!'
    
        }

    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 72ms

Describing Function: Get-FooBar
  [+] Should return expected text 31ms

Describing Function: Get-Bar
  [+] Should return expected text 49ms

I hope that clarified it a bit

Hi Evgeny

InModuleScope allow you to test internal functions in a module (functions that have not been exported)

Consider this module:

$m = New-Module -Name FooBar -ScriptBlock {

    function Get-Foo {
        'FOO!'
    }

    function Get-Bar {
        'BAR!'
    }
    
    function Get-FooBar {
        '{0} {1}' -f (Get-Foo), (Get-Bar)
    }

    Export-ModuleMember -Function Get-Foo, Get-FooBar

}
$m | Import-Module -Force

When you test it without InModuleScope, the test of the Get-Bar function will fail, as it has not been exported.

Describe 'Function: Get-Foo' {

    It 'Should return expected text' {
    
        Get-Foo | Should Be 'FOO!'
    
    }

}

Describe 'Function: Get-FooBar' {

    It 'Should return expected text' {
    
        Get-FooBar | Should Be 'FOO! BAR!'
    
    }

}

Describe 'Function: Get-Bar' {

    It 'Should return expected text' {
    
        Get-Bar | Should Be 'BAR!'
    
    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 66ms

Describing Function: Get-FooBar
  [+] Should return expected text 36ms

Describing Function: Get-Bar
  [-] Should return expected text 147ms
    CommandNotFoundException: The term 'Get-Bar' is not recognized as the name of a cmdlet, function, script file, or 
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try a
gain.
    at , : line 25

When you wrap it up in an InModuleScope block the internal function is made available for testing.

InModuleScope $m.Name {

    Describe 'Function: Get-Foo' {

        It 'Should return expected text' {
    
            Get-Foo | Should Be 'FOO!'
    
        }

    }

    Describe 'Function: Get-FooBar' {

        It 'Should return expected text' {
    
            Get-FooBar | Should Be 'FOO! BAR!'
    
        }

    }

    Describe 'Function: Get-Bar' {

        It 'Should return expected text' {
    
            Get-Bar | Should Be 'BAR!'
    
        }

    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 105ms

Describing Function: Get-FooBar
  [+] Should return expected text 83ms

Describing Function: Get-Bar
  [+] Should return expected text 42ms

But InModuleScope does not have to wrap all the tests, you can be as selective as you want.

Describe 'Function: Get-Foo' {

    It 'Should return expected text' {
    
        Get-Foo | Should Be 'FOO!'
    
    }

}

Describe 'Function: Get-FooBar' {

    It 'Should return expected text' {
    
        Get-FooBar | Should Be 'FOO! BAR!'
    
    }

}

Describe 'Function: Get-Bar' {

    InModuleScope $m.Name {
    
        It 'Should return expected text' {
    
            Get-Bar | Should Be 'BAR!'
    
        }

    }

}

Test result:

Describing Function: Get-Foo
  [+] Should return expected text 73ms

Describing Function: Get-FooBar
  [+] Should return expected text 27ms

Describing Function: Get-Bar
  [+] Should return expected text 37ms

I hope that clarifies it a bit

Hi Christian,

As I mentioned, this just does not work. Let me bring up an example:

We have a module (MyProcess.psm1):

Function Get-ProcessModule {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string]$Name
    )
    $Processes = Get-Process -Name $Name

    If ( $Processes ) {
        Foreach ( $Process in $Processes ) {
            $LoadedModules = $Process.Modules
            Foreach ( $LoadedModule in $LoadedModules ) {
                $CustomProps = @{
                    'Name'= $LoadedModule.ModuleName
                    'Version'= $LoadedModule.ProductVersion
                    'PreRelease' = $LoadedModule.FileVersionInfo.IsPreRelease
                }
            $CustomObj = New-Object -TypeName psobject -Property $CustomProps
            $CustomObj
            }
        }
    }
}

We have tests (MyProcess.tests.ps1):

$ScriptPath = "$PSScriptRoot\MyProcess.psm1"
Import-Module $ScriptPath

$JsonMockData = Get-Content -Path "$PSScriptRoot\MockObjects.json" -Raw
$Mocks = ConvertFrom-Json $JsonMockData

    Describe 'Get-ProcessModule' {
        Context 'There is 1 running process with the specified name' {

            $ContextMock = $Mocks.'Get-Process'.'1ProcessWithMatchingName'
            Mock Get-Process { $ContextMock } -ModuleName 'MyProcess'
            It 'Returns the correct module name' {
                (Get-ProcessModule -Name 'Any').Name |
                    Should Be $ContextMock.Modules.ModuleName
            }
            It 'Returns the correct module version' {
                (Get-ProcessModule -Name 'Any').Version |
                    Should Be $ContextMock.Modules.ProductVersion
            }
            It 'Returns the correct PreRelease value' {
                (Get-ProcessModule -Name 'Any').PreRelease |
                    Should Be $False
            }
        }
        Context 'There are 2 processes with the specified name' {

            $ContextMock = $Mocks.'Get-Process'.'2ProcessesWithMatchingName'
            Mock Get-Process { $ContextMock | Where-Object { $_ } }
            It 'Returns modules from both processes' {
                (Get-ProcessModule -Name 'Any').Count |
                    Should Be 2
            }
        }
    }

And test data (MockObjects.json):

{
    "Get-Process": [
        {
            "1ProcessWithMatchingName": {
                "Modules": {
                    "ModuleName": "Module1FromProcess1",
                    "ProductVersion": "1.0.0.1",
                    "FileVersionInfo": {
                        "IsPreRelease": false
                    }
                }
            }
        },
        {
            "2ProcessesWithMatchingName": [
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess1",
                        "ProductVersion": "1.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": false
                        }
                    }
                },
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess2",
                        "ProductVersion": "2.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": true
                        }
                    }
                }
            ]
        }
    ]
}

Now, this line

Mock Get-Process { $ContextMock } -ModuleName 'MyProcess'

does not work properly, because inside MyProcess.psm1 when mocked Get-Process is being called there is no access to $ContextMock variable. Therefore, the only way to access it is InModuleScope. And here my question from the first post pops-up…