Skip to content
6 min read

Proving Interactions with Should -Invoke

  • powershell
  • pester
  • testing
  • mocking
  • should-invoke

Some behavior isn't a returned value—it is the call. "Did it actually try to send the email?" "Did it call the delete exactly once and not twice?" A canned return value can't answer that. Pester records every call to a mocked command, and Should -Invoke lets you assert on those calls: how many times, in what scope, and (in Part 20) with what arguments.

What you'll learn

  • Asserting a mocked command was called with Should -Invoke
  • Why -Exactly matters alongside -Times
  • Using -Times 0 to prove something did not happen
  • Controlling what counts with -Scope
  • The legacy Assert-MockCalled name and why you should not use it

The basic assertion

Start with the disk-alert function from Part 17. Mock the side effect, run the function, then assert the call happened.

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'
        return 'alert-sent'
    }
    return 'ok'
}

Describe 'Send-DiskAlert' {
    BeforeAll {
        Mock Send-MailMessage { }   # neutralize the real send
    }

    It 'sends an alert when free space is low' {
        Send-DiskAlert -FreePercent 5 -To 'ops@example.com'

        Should -Invoke Send-MailMessage -Times 1 -Exactly
    }
}

Should -Invoke Send-MailMessage checks the recorded calls for the mocked command. -Times 1 -Exactly says: exactly one call, no more, no fewer. The mock body is empty, so no email is ever sent — we are asserting the intent to send, safely.

Always pair -Times with -Exactly

This is the single most important detail in this post.

Without -Exactly, the -Times number is a minimum, not an exact match. Should -Invoke Foo -Times 1 quietly means "at least once." So a buggy function that calls Send-MailMessage three times would still pass -Times 1 — the test would not catch the duplicate emails.

# WRONG: passes even if the command was called 1, 2, or 50 times
Should -Invoke Send-MailMessage -Times 1

# RIGHT: passes only if called exactly once
Should -Invoke Send-MailMessage -Times 1 -Exactly

Make -Exactly a habit whenever you mean an exact count. The only time you can safely drop it is when "at least N" is genuinely what you want.

Proving something did NOT happen

A -Times 0 -Exactly assertion is one of the most valuable tests you can write: it proves a side effect was avoided.

Describe 'Send-DiskAlert' {
    BeforeAll {
        Mock Send-MailMessage { }
    }

    It 'does not send an alert when free space is fine' {
        Send-DiskAlert -FreePercent 50 -To 'ops@example.com'

        Should -Invoke Send-MailMessage -Times 0 -Exactly
    }
}

This is how you guarantee your function does not email everyone when nothing is wrong, or does not delete files on the happy path. The negative assertion is often more important than the positive one.

Controlling what counts with -Scope

Should -Invoke counts calls within a scope. By default the scope is the current block, which is usually what you want when the assertion lives in the same It as the action. But you can be explicit:

  • -Scope It — count only calls made within the current It block.
  • -Scope Context — count all calls across the enclosing Context.
  • -Scope Describe — count all calls across the enclosing Describe.

This matters when the action happens in a shared BeforeEach or in a different test and you want to total things up at a wider level.

Describe 'logging on success' {
    BeforeAll {
        function Save-Record { 'Logging: record saved' | Out-Null; Write-Information 'saved' }
        Mock Write-Information { }
    }

    It 'logs once per save' {
        Save-Record
        Should -Invoke Write-Information -Times 1 -Exactly -Scope It
    }
}

When in doubt, keep the action and the Should -Invoke in the same It and let the default scope apply. Reach for -Scope Context/-Scope Describe only when you deliberately accumulate calls across tests.

You must mock the command to verify it

Should -Invoke reads Pester's call log, and Pester only logs calls to commands it has mocked. If you try to verify a command you never mocked, Pester has nothing to count and the assertion errors. The pattern is always two steps:

  1. Mock CommandName { ... } — so Pester intercepts and records the calls.
  2. Should -Invoke CommandName -Times N -Exactly — so you assert on what was recorded.

A note on the legacy name

In Pester v4, the verification command was Assert-MockCalled. In Pester 5 it still exists as an alias for backward compatibility, but Should -Invoke is the current, preferred form and the one you will see in modern examples and documentation.

# Pester 5 (preferred)
Should -Invoke Send-MailMessage -Times 1 -Exactly

# Legacy v4 alias (still works, avoid in new tests)
Assert-MockCalled Send-MailMessage -Times 1 -Exactly

Write Should -Invoke in new tests; recognize Assert-MockCalled when you meet it in older code.

Try it yourself

Take a function that logs (with Write-Information, Write-Verbose, or a custom logger) and has an early-exit path. Mock the logging command, then write two tests: one asserting it is invoked exactly once on the success path (-Times 1 -Exactly), and one asserting it is invoked zero times on the early exit (-Times 0 -Exactly).

Common mistakes

Forgetting -Exactly. Without it, -Times 1 means "at least one," so duplicate calls slip through. Add -Exactly whenever you mean an exact count.

Wrong -Scope. Asserting at -Scope It when the action happened in another test (or vice versa) yields a count that does not match. Keep action and assertion together, or set the scope deliberately.

Verifying a command you never mocked. Should -Invoke only counts mocked commands. Mock first, then verify.

Recap

Should -Invoke asserts on the calls Pester recorded for a mocked command. Always pair -Times N with -Exactly to mean an exact count, use -Times 0 -Exactly to prove a side effect was avoided, and set -Scope deliberately when you accumulate calls across tests. Prefer Should -Invoke over the legacy Assert-MockCalled alias.

Next up: Part 20 — Targeted Mocks with -ParameterFilter, where one command can behave differently depending on the arguments it receives.