Skip to main content

Verifying calls

A mock lets you control what a command returns. Should-Invoke lets you assert it was called at all — which is how you test behaviour that leaves no return value behind.

Should-Invoke

Add a third test to the Get-Planet with mocked data block, below the two you already have:

Planetarium/Public/Get-Planet.Mocking.Tests.ps1
Describe 'Get-Planet with mocked data' {
# ... BeforeAll and the two existing It blocks ...

It 'Filters the mocked data the same way' {
(Get-Planet -Name 'A*').Name | Should-Be 'Aiur'
}

It 'Reads the data source exactly once per call' {
Get-Planet | Out-Null

Should-Invoke Get-PlanetData -ModuleName Planetarium -Times 1 -Exactly
}
}

Should-Invoke does not install anything; it simply checks that the mock defined in BeforeAll is being called in this test.

Note the use of -Exactly. -Times 1 on its own means "at least once", so a function that read the file three times would pass. With -Exactly this test would catch a refactor that accidentally re-reads the CSV per planet — a bug that might be invisible to the user but reduce performance.

The -ModuleName rule from the previous page applies here too: you are asking about a mock that lives in the module's scope, so you have to say so.

Assert on calls only when the call is the behaviour

Should-Invoke is the right tool for "it wrote to the log", "it retried twice", "it did not delete anything". It is the wrong tool for things you can check by looking at the result. Asserting on both the return value and the exact call sequence tends to produce tests that fail every time you tidy up the implementation, without ever catching a real bug.

Targeting specific calls

-ParameterFilter narrows a mock to calls whose arguments match, allowing you to customize responses for different calls. Let's give it a try:

Planetarium/Private/Get-PlanetData.Tests.ps1
Describe 'Get-PlanetData' -Tag 'Internal' {
# ... the 'Converts the CSV strings into numbers' test ...

It 'Reads the CSV shipped with the module' {
Mock -ModuleName Planetarium Import-Csv {
@([PSCustomObject] @{ Name = 'Aiur'; Order = '1'; DistanceFromSunKm = '100' })
} -ParameterFilter { $Path -like '*planets.csv' }

InModuleScope Planetarium { (Get-PlanetData).Name | Should-Be 'Aiur' }

Should-Invoke Import-Csv -ModuleName Planetarium -Times 1 -Exactly
}
}

Inside the filter, parameters are available as variables — $Path is whatever was passed as -Path. The mock only applies when the filter returns true, so this one says: intercept reads of files ending with planets.csv, and nothing else.

That doubles as an assertion. If the module ever reads a different file, the filter stops matching.

Mocks do not fall through

A mock with a -ParameterFilter only applies to calls that match it. If a call reaches a mocked command and nothing matches, Pester throws rather than guessing — it will not quietly run the real command behind your back.

Let's break it on purpose to see how this works. Change planets to moons in the test you just added:

Planetarium/Private/Get-PlanetData.Tests.ps1
It 'Reads the CSV shipped with the module' {
Mock -ModuleName Planetarium Import-Csv {
@([PSCustomObject] @{ Name = 'Aiur'; Order = '1'; DistanceFromSunKm = '100' })
} -ParameterFilter { $Path -like '*planets.csv' }
} -ParameterFilter { $Path -like '*moons.csv' }

# ... rest of the test unchanged ...
}
Invoke-Pester -Path ./Planetarium/Private/Get-PlanetData.Tests.ps1 -Output Detailed
Describing Get-PlanetData
[+] Converts the CSV strings into numbers 49ms
[-] Reads the CSV shipped with the module 29ms
RuntimeException: No mock for command 'Import-Csv' matched the call: none of the parameter filters matched, and there is no default mock to fall back to. Add a default mock (e.g. `Mock Import-Csv { ... }`) or adjust an existing -ParameterFilter.
The following parameter filters were evaluated and did not match:
{ $Path -like '*moons.csv' } bound parameters: Path = /home/you/pester-tutorial/Planetarium/Private/../Data/planets.csv

Read the last line: it shows the filter that was evaluated and the arguments it was evaluated against. The filter wanted *moons.csv, the actual path ends in planets.csv — the mismatch is right there, with no guessing required.

This behavior is new in Pester v6. Previous versions called the original command when the filter failed. Tests could still pass based on the real data, and you would trust a mock that was never used.

Giving a mock a fallback

Sometimes you genuinely want "handle this specific case, and everything else generically". Say so explicitly by adding a second mock with no -ParameterFilter — an unfiltered mock matches any call, so it becomes the fallback:

Mock Get-Thing { 'default' } # everything else
Mock Get-Thing { 'one' } -ParameterFilter { $Id -eq 1 } # the specific case

The example above is only used for illustration. Your Import-Csv mock should intercept exactly one file, so leaving it filtered and unmatched-is-an-error is the behaviour you want.

tip

See Mocking for an example of using the default mock to call the original command.

Change moons back to planets before moving on.

Running the suite

./test.ps1
Tests Passed: 20, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

Twenty tests: the original fifteen, plus five that no longer care what is in planets.csv.

There is also Should-NotInvoke, the mirror image, for asserting a command was not called — "it did not delete anything", "it did not retry". It takes the same -ModuleName and -ParameterFilter parameters.

Before you move on

0/5

Next module: testing functions that generate files without causing a mess.