Search code examples
powershellpester

Trouble mocking Powershell function with Pester that takes a Pipeline parameter


I am receiving the following error when trying get this pester test to work for a function that receives a pipeline parameter.

"An error occured running Pester: The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input."

Module: MyModule.psm1

    function MyFunction {

    "piping to Write-Output" | Write-Output 
   
    $dataTable = New-Object System.Data.DataTable 
    $dataTable | MyPipelineFunction 
    
}

function MyPipelineFunction {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true, 
            ValueFromPipeline=$true)]
        [System.Data.DataTable] $DataTable
    )
    Write-Host "hello world"
}

Pester Test

Import-Module "C:\temp\StackOverflow\PipelineMock\MyModule\MyModule.psm1" -Force

InModuleScope 'MyModule' {
    Describe 'MyFunction' {
        BeforeAll {

            # Create a sample DataTable
            $dataTable = New-Object System.Data.DataTable
            $dataTable.Columns.Add("Column1", [System.String])
            $row = $dataTable.NewRow()
            $row["Column1"] = "Sample Data"
            $dataTable.Rows.Add($row)

            # Mock the DataTable creation in MyInternalFunction
            Mock -CommandName New-Object -ParameterFilter { $TypeName -eq 'System.Data.DataTable' } -MockWith { return $dataTable }
    
            Mock MyPipelineFunction -MockWith {[CmdletBinding()] param (
                [Parameter(
                    Mandatory = $true, 
                    ValueFromPipeline=$true)]
                [System.Data.DataTable] $DataTable
                )     
            }

            Mock Write-Output {} 
        }

        It 'should call MyPipelineFunction' {

            MyFunction
            Assert-MockCalled -CommandName Write-Output -Exactly 1 -Scope It
            Assert-MockCalled -CommandName New-Object -Exactly 1 -Scope It -ParameterFilter {$TypeName -eq 'System.Data.DataTable' } 
            Assert-MockCalled -CommandName MyPipelineFunction -Exactly 1 -Scope It -ParameterFilter {$DataTable -eq $dataTable}  
        } 
    }
}

I've several different Mock approaches and parameter filters given what I found out there. The Pester site doesn't seem to have any clear guidance on this. I was able to write a basic mock for Write-Output where I piped text to it and that works fine.

Can anyone see the problem?


Solution

  • Note that Assert-MockCalled is obsolete, the replacement would be Should -Invoke. See the documentation for details.


    There 3 things you must know before showing you how the Pester code should look:

    1. What Mathias mentioned in his comment, even tho DataTable is not an IEnumerable the pipeline will enumerate it, essentially by enumerating the DataRowCollection, see Binders.cs#L613-L638. So, If you want to pipe it, and your function to receive it as-is, you have to either use Write-Output -NoEnumerate or the , operator:

      function MyFunction {
          'piping to Write-Output' | Write-Output 
          $dataTable = New-Object System.Data.DataTable 
          , $dataTable | MyPipelineFunction 
      }
      

      The same consideration will apply in your Mock -CommandName New-Object call, you will have to use -MockWith { , $dataTable } there (notice the , before the variable).

      This enumeration behavior mentioned for the pipeline also applies when Pester invokes the -MockWith scriptblock, making its output to be enumerated, to put it visually:

      $shouldBeDT = & {
          $dataTable = New-Object System.Data.DataTable
          $null = $dataTable.Columns.Add('Column1', [System.String])
          $row = $dataTable.NewRow()
          $row['Column1'] = 'Sample Data'
          $dataTable.Rows.Add($row)
          $dataTable
      }
      
      $shouldBeDT.GetType() # But is a `DataRow`
      
    2. You should not define a param block in your -MockWith scriptblock, as suggested in the documentation:

      NOTE: Do not specify param or dynamicparam blocks in this script block. These will be injected automatically based on the signature of the command being mocked, and the MockWith script block can contain references to the mocked commands parameter variables.

    3. You're asserting that { $DataTable -eq $dataTable }, you should note that PowerShell is a case-insensitive language, $DataTable and $dataTable refer to the same variable in this case. In addition, equality comparison using the -eq operator won't work to tell if both, the DataTable used as argument for your function and the DataTable created in the BeforeAll block are the same. In this case you could use [object]::ReferenceEquals, however you will need to use a different variable name in your test.

    In summary, the Pester test that should solve the problem:

    InModuleScope 'MyModule' {
        Describe 'MyFunction' {
            BeforeAll {
                $dt = New-Object System.Data.DataTable
                $dt.Columns.Add('Column1', [System.String])
                $row = $dt.NewRow()
                $row['Column1'] = 'Sample Data'
                $dt.Rows.Add($row)
    
                # Mock the DataTable creation in MyInternalFunction
                Mock New-Object -ParameterFilter { $TypeName -eq 'System.Data.DataTable' } -MockWith { , $dt }
                Mock Write-Output {}
                Mock MyPipelineFunction {}
            }
    
            It 'should call MyPipelineFunction' {
                MyFunction
                Assert-MockCalled Write-Output -Exactly 1 -Scope It
                Assert-MockCalled New-Object -Exactly 1 -Scope It -ParameterFilter {
                    $TypeName -eq 'System.Data.DataTable'
                }
                Assert-MockCalled MyPipelineFunction -Exactly 1 -Scope It -ParameterFilter {
                    [object]::ReferenceEquals($DataTable, $dt)
                }
            }
        }
    }