Pester icon indicating copy to clipboard operation
Pester copied to clipboard

Can't mock a function that takes pipeline input

Open fsackur opened this issue 3 years ago • 9 comments

Describe your environment

Pester 5.3.1 on PSv5 and v7 on Win 10

Steps to reproduce

function outer
{
    1..3 | inner
}

function inner
{
    param
    (
        [Parameter(ValueFromPipeline)]$InputObject
    )

    # implicit end block, runs once
    "RETURN"
}

Describe "unmocked" {
    It "runs the end block" {
        (outer).Count | Should -Be 1
    }
}

Describe "mocked" {
    It "runs the end block" {
        Mock inner {"FOO"}
        (outer).Count | Should -Be 1
    }
}

Expected Behavior

Both tests pass

Current Behavior

Second test fails (because mock returns 1 object for each piped input)

Possible Solution? (optional)

Steppable pipeline. For sample code, run [System.Management.Automation.ProxyCommand]::Create([System.Management.Automation.CommandMetadata]::new((Get-Command New-Item)))

fsackur avatar Apr 06 '22 22:04 fsackur

This is a long-standing limitation of Mock. It is not impossible. Just complicated to do.

nohwnd avatar Apr 12 '22 18:04 nohwnd

An alternative workaround may be to wrap a wrapper for testing.

function Wrapper{
    param(  
        [Parameter(Mandatory = $true)]
        [string]$str
    )
    return inner -str $str | outer
}

Describe "mocked" {
    BeforeEach {
        Mock Wrapper {}
    }
    It "testing" {
        # ...
    }
}

Bigpop avatar Sep 28 '22 01:09 Bigpop

Nowind, please explain your above comment. Is there documentation on how to do this? I can't seem to find any.

This may be complicated, but it's def a requirement if you want to use Pester to test any existing powercli code.

JesseDarr avatar Apr 18 '23 17:04 JesseDarr

My comment was supposed to mean: It is technically possible to implement this into Mock, but it is difficult.

I am not 100% sure about the use cases for this, and how hard or easy it would be to keep it backwards compatible.

I quickly looked at the implementation yesterday, and we can know if the function has begin, process or end by looking at ast:

function A {
    begin
    { 

    }

    process
    {

    }

    end
    {

    }
}

$a = Get-Command A
$a.ScriptBlock.Ast
$a.ScriptBlock.Ast.Body

function B {}
$b = Get-Command B
$b.ScriptBlock.Ast
$b.ScriptBlock.Ast.Body

Which outputs:

Attributes         : {}
UsingStatements    : {}
ParamBlock         :
BeginBlock         :
ProcessBlock       :
EndBlock           :
DynamicParamBlock  :
ScriptRequirements :
Extent             : {}
Parent             : function B {}

Where BeginBlock, ProcessBlock and EndBlock are populated.

I have to look at how steppable pipeline works because I've read about it many years ago but never had the need to use it since.

It would be nice to have multiple examples of how people want to use this before implementation is started, because honestly the Mock code is very complicated.

nohwnd avatar May 10 '23 07:05 nohwnd

Here's an example. I'm trying to mock RemoveDnsServerResourceRecord, which accepts objects returned by Get-DnsServerResourceRecord as input:

        [Object[]] $records = Get-DnsServerResourceRecord -Name $curName @dnsArgs -ErrorAction Ignore
        foreach ($record in $records)
        {
            if ($record.RecordType -ne 'CNAME' -or $record.RecordData.HostNameAlias -ne $HostNameAlias)
            {
                continue
            }

            Write-Information "    - $($record.HostName)  $($record.RecordType)  $($record.RecordData.HostNameAlias)"
            $record | Remove-DnsServerResourceRecord @dnsArgs -Force
        }

splatteredbits avatar May 18 '23 23:05 splatteredbits

Okay, been looking a bit more into the implementation of Mock, what we do now is that in begin we setup the mock, in process we run the MockWith scriptblock for every item that was received via pipeline, and in end we just check if we should invoke the original command, and if yes we invoke it.

From the examples above I am assuming that people want to do:

If there is just end block:

  1. autodetect if the mocked function has begin and process and use mockWith to replace just that?
  2. be able to assert (via Should -Invoke) on the number of calls of the end block?

If there is begin process and end:

  1. replace the end block of the mocked function and get all the input that was processed by the pipeline?
  2. replace the begin block as well?

nohwnd avatar Jun 30 '23 15:06 nohwnd