Testing with InModuleScope

Welcome Forums Pester Testing with InModuleScope

This topic contains 3 replies, has 2 voices, and was last updated by

 
Participant
6 months, 1 week ago.

  • Author
    Posts
  • #98359

    Participant
    Points: 0
    Rank: Member

    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.

  • #98373

    Participant
    Points: 0
    Rank: Member

    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

  • #98374

    Participant
    Points: 0
    Rank: Member

    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

  • #98596

    Participant
    Points: 0
    Rank: Member

    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...

The topic ‘Testing with InModuleScope’ is closed to new replies.