Mocking Missing Cmdlet Pipelines with Pester

Following on from the previous the Mocking Missing Cmdlets with Pester and Mocking Missing Cmdlet ErrorAction with Pester posts, I also encountered (yet) another interesting problem when attempting to mock cmdlets that were not present on the test system. This one is pretty similar to the -ErrorAction edge-case but involves the pipeline. Here’s a pseudo test that was failing:

 
Describe 'Mocking Missing Cmdlet Pipeline Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    Function Remove-VM { [CmdletBinding()] param ($Name) }
    
    InModuleScope 'xVMHyper-V' {
 
        It 'Calls Remove-VM' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { return 'TestVM' }
            Mock Remove-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { }
            Get-VM -Name 'TestVM' | Remove-VM;
            Assert-MockCalled Remove-VM -Scope It;
        }
    }
}

In the above example, we wish to mock both the Get-VM and Remove-VM cmdlets and assert that the Remove-VM cmdlet is called. If we run this on a system that does not have the Hyper-V cmdlets installed we receive the following error:

Executing all tests in 'C:\Users\Iain\Desktop\TestPipeline.Tests.ps1'
Describing Mocking Pipeline Example
Remove-VM : 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.
At C:\Users\Iain\Desktop\TestPipeline.Tests.ps1:11 char:37
+             Get-VM -Name 'TestVM' | Remove-VM;
+                                     ~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (TestVM:String) [Remove-VM], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectNotBound,Remove-VM

 [-] Calls Remove-VM 1.35s
   Expected Remove-VM to be called at least 1 times but was called 0 times
   at line: 518 in C:\Users\Iain\OneDrive\Powershell\Modules\Pester\Functions\Mock.ps1
Tests completed in 1.35s
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

The error message is fairly self-explanatory. Just like your standard Advanced Function definition, we need to indicate that a parameter needs to be able to accept input via the pipeline using the ValueFromPipeline attribute. To fix this we just need to add the Parameter attribute to our stub function definition:

 
Describe 'Mocking Pipeline Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    Function Remove-VM { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Name) }
    
    InModuleScope 'xVMHyper-V' {
 
        It 'Calls Remove-VM' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { return 'TestVM' }
            Mock Remove-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { }
            Get-VM -Name 'TestVM' | Remove-VM;
            Assert-MockCalled Remove-VM -Scope It;
        }
    }
}

Whilst it’s not rocket-science – just in case someone else runs into it – I thought it would be worth quickly documenting! Here’s the working example:

Executing all tests in 'C:\Users\Iain\Desktop\TestPipeline.Tests.ps1'
Describing Mocking Pipeline Example
 [+] Calls Remove-VM 74ms
Tests completed in 74ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

 

Mocking Missing Cmdlet ErrorAction with Pester

Following on from the previous Mocking Missing Cmdlets with Pester post, I also encountered another interesting problem when attempting to mock cmdlets that were not present on the test system. This one is more of an edge-case, hence its own post. Just like last time, the tests worked when I had the Hyper-V cmdlets installed, but failed when running within an Appveyor VM.

It’s probably not uncommon that you will need to ensure that the code under test should throw an error here-and-there. Here is a pseudo-example that tests that Get-VM writes an error when passed with a non-existent VM name:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

When this test is run it fails:

Describing Mocking ErrorAction Example
 Write-Error 'Oops'  : Oops
At C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1:709 char:21
+                     & $___ScriptBlock___ @___BoundParameters___ @___ArgumentList ...
+                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException 
 [-] Get-VM Throws 176ms
   Expected: the expression to throw an exception
   at line: 6 in D:\Users\Iain\Desktop\PesterDemo.Tests.ps1
Tests completed in 176ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

The problem here is that the stub function is not an advanced function and the –ErrorAction preference switch is ignored! This is easily resolved by adding the [CmdletBinding()] attribute to the stub function definition:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

Running the test now results in the expected output:

Describing Mocking ErrorAction Example
 [+] Get-VM Throws 177ms
Tests completed in 177ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

This is not a Pester issue and it’s not a Powershell issue either. It’s just the way the normal Powershell functions work. But, just in case someone else runs into it I thought it would be worth quickly documenting!

Mocking Missing Cmdlets with Pester

When writing Pester unit tests for your Powershell code you will probably have a need to mock calls to external functions before too long. This process works as you would expect when Pester can locate a defined function/cmdlet with a matching name. However, if Pester cannot find a definition, it will fail.

This problem will normally surface in a Continuous Integration (CI) environment. For me, it was writing the first suite of tests for the xHyper-V DSC resource module. The Hyper-V cmdlets where present on my authoring machine but were not present on the Appveyor build VM.

Here is an overly simplified Pester test that I’ll use for demonstration purposes:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

If we run the test and Pester cannot locate a defined function then it will report an error. Note: if you run this on a machine with the Hyper-V module installed (and you have a VM called ‘TestVM’) then it will pass – but you knew that already ;).

Describing Missing Cmdlet Mocking Example
 [-] calls Get-VM 474ms
   Could not find Command Get-VM
   at line: 600 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 474ms

This is easily overcome by defining an empty function within the test file. Note: this will need to be defined within the ‘InModuleScope’ script block if you’re testing a module’s internals.

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

The test now passes as we would expect. Yay \o/

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 141ms
Tests completed in 141ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Now, what you really need to know is that for Pester to enumerate and mock parameter filters, those parameters need to be defined on the stub function. If we were to update the test to check for the passing of a particular –Name parameter like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

When we run the test it will now fail again.

Describing Missing Cmdlet Mocking Example
 [-] Calls Get-VM 131ms
   Expected Get-VM to be called at least 1 times but was called 0 times
   at line: 518 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 131ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

For Pester to enumerate the dynamic parameters on the function, it needs to have the parameters (only the one’s you’re interested in) defined. This can easily be fixed like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { param ($Name) }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

The tests will now once again pass successfully!

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 169ms
Tests completed in 169ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Hopefully this helps someone and saves some time. It took me a while to work out what was going on as I had the cmdlets available on my development machine but the tests were failing when running in an Appveyor VM. Perhaps I should submit a pull request to get this put into the Pester help documentation?!