3

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

scorpio
  • 1,587
  • 2
  • 15
  • 27

1 Answers1

4

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

alx9r
  • 3,675
  • 4
  • 26
  • 55
  • alx9r, thanks for the answer, I totally understand why it's not working and exactly had in mind what you explained. I have tried the call operator with same failure. I also have tried calling function using the statement $logger.Invoke($logText, $verbosityLevel, $logType), but this also result in same failure. – scorpio Feb 26 '16 at 14:01
  • @scorpio I just had a chance to run your code. The culprit was `${function:Common-Write-Log-Console}`. I updated my answer. Both tests now pass on my machine. – alx9r Feb 26 '16 at 23:59
  • Hi alx9r, many thanks for clarifying, it resolves my understanding of delegates in power shell. When debugging I was wondering why the whole function body was being copied in the $loggers["Console_Logger"] when using the old statement "$loggers.Add("Console_Logger", ${function:Common-Write-Log-Console}) – scorpio Feb 29 '16 at 09:00