Skip to content
6 min read

Targeted Mocks with -ParameterFilter

  • powershell
  • pester
  • testing
  • mocking
  • parameterfilter

So far your mocks have been blunt instruments: mock Get-Content, and every call to Get-Content returns the same canned value, no matter what path it was given. Real functions are pickier than that. Sometimes you need one path to succeed and another to fail, or you need to prove the command was called with a specific argument—not just called at all. -ParameterFilter is how you make a mock argument-aware.

What you'll learn

  • Scoping a mock to specific arguments with -ParameterFilter
  • Defining several mocks of one command, each keyed on different arguments
  • Using -ParameterFilter on Should -Invoke to verify a precise call
  • Referencing the bound parameters of the call correctly inside the filter

A filter narrows when a mock applies

-ParameterFilter takes a scriptblock that returns $true or $false. When the mocked command is called, Pester runs the filter against that call's arguments. If the filter returns $true, this mock handles the call. If it returns $false, this mock stands aside and Pester looks for another match (or falls through to the real command).

Inside the filter you reference the call's arguments by their parameter names, as automatic variables. A call of Test-Path -Path 'C:\a' exposes $Path inside the filter.

Describe 'Test-Path filtered' {
    It 'returns true only for the path we care about' {
        Mock Test-Path { $true } -ParameterFilter { $Path -eq 'C:\config\app.json' }

        Test-Path -Path 'C:\config\app.json' | Should -BeTrue
    }
}

The mock fires only when $Path equals that exact string. A call with any other path does not match this mock.

Several mocks, one command

This is where filters shine: you can register the same command multiple times, each with a different filter, so it behaves differently per argument. Pair it with a final unfiltered mock to act as the default.

Describe 'Test-Path branching' {
    BeforeAll {
        # Most specific filters first, broad default last.
        Mock Test-Path { $true }  -ParameterFilter { $Path -eq 'C:\exists' }
        Mock Test-Path { $false } -ParameterFilter { $Path -eq 'C:\missing' }
        Mock Test-Path { $false }   # default for anything else
    }

    It 'reports the existing path as present' {
        Test-Path -Path 'C:\exists' | Should -BeTrue
    }

    It 'reports the missing path as absent' {
        Test-Path -Path 'C:\missing' | Should -BeFalse
    }
}

Now a single command models a whole filesystem's worth of answers, entirely in memory. Your function under test can branch on Test-Path results and you control each branch.

Match order: most recent matching filter wins

When more than one mock could match a call, Pester uses the most recently defined matching mock. The unfiltered Mock Test-Path { $false } matches everything, so put it last as a catch-all default. The safe habit: define specific filters first, the default after them, and never let two filters overlap on the same arguments.

A worked example: two paths through one function

Here is a function that checks for a config file and falls back to a default location. We want to test both branches without touching disk.

function Get-ConfigPath {
    param([string]$Preferred, [string]$Fallback)

    if (Test-Path -Path $Preferred) { return $Preferred }
    if (Test-Path -Path $Fallback)  { return $Fallback }
    throw 'No config file found.'
}

Describe 'Get-ConfigPath' {
    It 'uses the preferred path when it exists' {
        Mock Test-Path { $true }  -ParameterFilter { $Path -eq 'C:\app\config.json' }
        Mock Test-Path { $false } -ParameterFilter { $Path -eq 'C:\app\default.json' }

        Get-ConfigPath -Preferred 'C:\app\config.json' -Fallback 'C:\app\default.json' |
            Should -Be 'C:\app\config.json'
    }

    It 'falls back when the preferred path is missing' {
        Mock Test-Path { $false } -ParameterFilter { $Path -eq 'C:\app\config.json' }
        Mock Test-Path { $true }  -ParameterFilter { $Path -eq 'C:\app\default.json' }

        Get-ConfigPath -Preferred 'C:\app\config.json' -Fallback 'C:\app\default.json' |
            Should -Be 'C:\app\default.json'
    }
}

Two mocks, keyed on path, let one test exercise the "preferred wins" branch and another the "fall back" branch — deterministically, offline.

Filtering Should -Invoke too

-ParameterFilter is not just for the mock definition. You can attach it to Should -Invoke to assert that a command was called with particular arguments, which is far stronger than "called at all."

Describe 'Send-DiskAlert targets the right inbox' {
    BeforeAll {
        function Send-DiskAlert {
            param([int]$FreePercent, [string]$To)
            if ($FreePercent -lt 10) {
                Send-MailMessage -To $To -From 'alerts@example.com' `
                    -Subject "Low disk: $FreePercent% free" -SmtpServer 'smtp.example.com'
            }
        }
        Mock Send-MailMessage { }
    }

    It 'emails the operations inbox specifically' {
        Send-DiskAlert -FreePercent 5 -To 'ops@example.com'

        Should -Invoke Send-MailMessage -Times 1 -Exactly `
            -ParameterFilter { $To -eq 'ops@example.com' }
    }
}

This asserts the email went to ops@example.com exactly once. If your code used the wrong address, the count for this filter would be zero and the test would fail — even though Send-MailMessage was technically called.

Referencing the right variable

The filter sees the call's bound parameters by their parameter names. Two things follow:

  • Use the parameter name as the source defines it. If the cmdlet's parameter is -Path, the variable is $Path. If your code calls Get-Item -LiteralPath $x, the variable is $LiteralPath, not $Path.
  • To inspect everything at once, the hashtable $PesterBoundParameters holds all bound arguments — handy for debugging a filter that never matches.

Try it yourself

Take a function that calls Test-Path, Get-Item, or Get-ChildItem against more than one location. Write a Describe that mocks the command twice with -ParameterFilter — one path returning $true, another returning $false — and add a test for each branch. Then add a Should -Invoke ... -ParameterFilter { ... } proving the call was made against the path you expected.

Common mistakes

Wrong parameter name in the filter. -ParameterFilter { $FilePath -eq ... } when the real parameter is -Path will never match, so the mock silently does not apply. Match the exact parameter name the code uses.

Overlapping filters. If two filters can both return $true for the same call, the most recently defined one wins — which is easy to get backwards. Keep filters mutually exclusive, and put the unfiltered catch-all last.

Referencing a non-bound variable. The filter can only see parameters that were actually bound on the call. A parameter the caller left at its default value is not bound, so testing it in the filter compares against $null.

Recap

-ParameterFilter turns a blunt mock into a precise one: its scriptblock returns $true/$false against the call's bound parameters, letting one command behave differently per argument and letting Should -Invoke verify a specific call. Order specific filters before the catch-all, keep them non-overlapping, and reference parameters by their real names. You now have the full mocking toolkit — but mocks have sharp edges, especially inside modules.

Next up: Part 21 — Mocking Pitfalls: Scope, InModuleScope, and Over-Mocking, where we fix the classic "my mock isn't taking" problem and learn when not to mock at all.