Search code examples
unit-testingpowershelldependency-injectioninvoke-commandpester

Pester Mock does not work for Invoke-Command using script block


I have a console logger

function Common-Write-Log-Console
{
    param (
        [Parameter(Mandatory=$true)]
        [string] $logText
        )

        $textToOutput = [String]::Format("{0}:{1}", [System.DateTime]::Now.ToString(), $logText)
        Write-Output ($textToOutput)
}

Then I have wrapper function which calls it by dynamically loading it

function Common-Write-Log-WithInvoke
{
    param (
        [Parameter(Mandatory=$true)]
        [string] $logText

        )

    foreach($logger in $loggers.Values)
    {
        Invoke-Command $logger -ArgumentList $logText,$verbosityLevel,$logType
    }

}

Another wrapper function which calls it directly

function Common-Write-Log-WithoutInvoke
{
    param (
        [Parameter(Mandatory=$true)]
        [string] $logText, 
        [string] $verbosityLevel = "Normal",
        [string] $logType = "Info"
        )

    Common-Write-Log-Console $logText

}

Add Loggers for dynamic calling

 $loggers = @{}
 $loggers.Add("Console_Logger", ${function:Common-Write-Log-Console})

Now I have couple of Pester tests

 # pester tests
Describe "Common-Write-Log" {
    It "Test 1. Calls all log sources when log sources are called directly - **this test passes**" {


        # Arrange
        $expectedLogText  = "test message" 
        Mock Common-Write-Log-Console -Verifiable -ParameterFilter { $logText -eq  $expectedLogText}

        # Act
        Common-Write-Log-WithoutInvoke "test message"

        # Assert
        Assert-VerifiableMocks
    }

    It "Test 2. Calls all log sources when log sources are called through Invoke-Command - **this test fails**" {


        # Arrange
        $expectedLogText  = "test message" 
        Mock Common-Write-Log-Console -Verifiable -ParameterFilter { $logText -eq  $expectedLogText}

        # Act
        Common-Write-Log-WithInvoke "test message"

        # Assert
        Assert-VerifiableMocks # This statement fails as actual function "Common-Write-Log-Console" is called instead of the mocked one
    }
}

Test 2. always fails. I have worked around by creating a fake logger function, instead of using mock and setting some global variables to verify/assert in my test that dynamic loading and calling of intended function works. It would be nice to get the Mock working in such scenario , rather then writing those dumb fakes!

Any ideas how would it work or is it not supported by pester at all?

PS: All code works if copied in order


Solution

  • Pester's Scope of Mocked Function Interception

    Pester only intercepts calls to mocked functions in particular scopes. I think the only supported method of controlling this scope is using InModuleScope. That allows you to designate that Pester should intercept calls to mocked functions in the module that you have specified using InModuleScope.

    Common-Write-Log-Console is Not Called in a Scope where Pester Intercepts

    In "Test 2.", the "call" to Common-Write-Log-Console takes place somewhere inside this call:

    Invoke-Command $logger -ArgumentList $logText,$verbosityLevel,$logType
    

    You have not specified that Pester should intercept calls to mocked functions inside whatever module Invoke-Command is implemented. (I doubt that you could achieve this, because Invoke-Command is shipped with WMF and probably not implemented in PowerShell.)

    Use the Call Operator Instead of Invoke-Command

    When invoking PowerShell commands as delegates I recommend using the & call operator instead of Invoke-Command. If you rewrite this line

    Invoke-Command $logger -ArgumentList $logText,$verbosityLevel,$logType
    

    as

    & $logger -logText $logText
    

    Test 2, should call the mock for Common-Write-Log-Console as you desire.

    A Handle to a PowerShell Delegate is Just a String Containing the Function Name

    When invoking a PowerShell delegate, all you need is a string containing the name of the function. If you rewrite this line

    $loggers.Add("Console_Logger", ${function:Common-Write-Log-Console})    
    

    as

    $loggers.Add("Console_Logger", 'Common-Write-Log-Console')
    

    $logger will correctly contain the name of the command which the call operator can invoke.


    I tested this on my computer and both tests now pass:

    enter image description here