Skip to content

Can't mock a function that takes pipeline input #2154

Open

Description

@fsackur

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

Activity

  1. nohwnd commented on Apr 12, 2022

    @nohwnd
    Member

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

  2. Bigpop commented on Sep 28, 2022

    @Bigpop

    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" {
            # ...
        }
    }
    
  3. JesseDarr commented on Apr 18, 2023

    @JesseDarr

    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.

  4. nohwnd commented on May 10, 2023

    @nohwnd
    Member

    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.

  5. splatteredbits commented on May 18, 2023

    @splatteredbits
    Contributor

    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
            }
  6. nohwnd commented on Jun 30, 2023

    @nohwnd
    Member

    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?
  7. jazzdelightsme commented on Nov 21, 2024

    @jazzdelightsme

    My scenario is that I was trying to mock Set-Content, but it broke for 'a', 'b' | Set-Content .\test.txt. In my mock, I provided begin, process, and end blocks, which were called, but the problem is that the whole command was invoked twice (begin, process, end ; begin , process, end) instead of once with two calls to the process block (begin, process, process, end).

    (Edit: tried on 5.6.1 and 6.0.0-alpha5)

  8. SimonEData commented on Jan 3, 2025

    @SimonEData

    As a work-around you may be able to use Mock with -ParameterFilter to only return something from the mock when the last item is received from the pipeline, if you know what that last item is (which presumably you should do when writing tests).

    Using the original example, modified to add a "mocked with ParameterFilter" Describe block that works:

    BeforeAll {
        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
        }
    }
    
    Describe "mocked with ParameterFilter" {
        It "runs the end block" {
            Mock inner {  } -ParameterFilter { $InputObject -ne 3 }
            Mock inner {"FOO"} -ParameterFilter { $InputObject -eq 3 }
            (outer).Count | Should -Be 1
        }
    }

    The "unmocked" and "mocked with ParameterFilter" tests both work. The "mocked" test fails since (outer).Count is 3.

  9. powercode commented on May 27, 2025

    @powercode
    Contributor

    I have a use case for this which is to test that I pass the correct command to an invocation of pwsh.

     $testParams = @(
            '-TestVerbosity', $TestVerbosity
            if ($Include) { '-Include', $Include }
            if ($Tag) { ('-Tag', $Tag) }
            if ($CodeCoverage) { '-CodeCoverage' }
            if ($Path) { '-Path', ($Path | Join-String -Separator:',') }
            '-ArtifactsPath', $artifactsPath
            '-PassThru'
        )
        $common = [BoundParameters]::GetAllCommonBoundParameters($PSBoundParameters)
        $testParams += [BoundParameters]::ToStringArray($common)
        [string] $command = "$engPath\RunAllTests.ps1 $testParams"
    
        $command | pwsh -noprofile -noninteractive -command -

    I want to mock pwsh to validate the pipeline input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions