Skip to content
6 min read

Mocking Pitfalls: Scope, InModuleScope, and Over-Mocking

  • powershell
  • pester
  • testing
  • mocking
  • inmodulescope

You wrote a mock. You ran the test. The real command ran anyway—the email went out, the file got deleted, the test failed for reasons that make no sense. Mocking is the part of Pester beginners get most wrong, and almost every confusing failure traces back to one idea: scope. This post walks the classic traps and shows you how to step around each one.

What you'll learn

  • Why a Mock "doesn't take" for a command called inside a module
  • Fixing module-scope mocking with InModuleScope
  • Testing private (non-exported) functions
  • Recognizing over-mocking as a design smell
  • When a real integration test serves you better than a mock

The trap: a mock that doesn't take

Mock replaces a command within the scope where the mock is defined. When your function lives in a .ps1 you dot-sourced, it runs in the test's scope, so a mock defined in the test reaches it. Simple.

Modules are different. A module has its own internal scope, and commands its functions call resolve inside that scope — not the test's. So a mock defined in your test script lives in the test scope, and the call inside the module never sees it.

Here is a tiny module to make it concrete. Save it as Mailer.psm1:

function Send-Welcome {
    param([string]$To)
    Send-MailMessage -To $To -From 'hi@example.com' `
        -Subject 'Welcome' -SmtpServer 'smtp.example.com'
    return 'sent'
}
Export-ModuleMember -Function Send-Welcome

Now the naive test, which fails (it actually tries to send mail):

Describe 'Send-Welcome (broken mock)' {
    BeforeAll {
        Import-Module $PSScriptRoot/Mailer.psm1 -Force
    }

    It 'does not really send' {
        Mock Send-MailMessage { }            # defined in TEST scope

        Send-Welcome -To 'new@example.com'   # calls Send-MailMessage in MODULE scope

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

The mock is in the test scope; Send-Welcome calls Send-MailMessage from inside the module, where the mock does not exist. The real command runs and Should -Invoke reports zero calls.

The fix: tell Mock which module to reach into

You do not have to wrap everything. The cleanest fix is the -ModuleName parameter on Mock (and on Should -Invoke), which injects the mock into the named module's scope:

Describe 'Send-Welcome (fixed with -ModuleName)' {
    BeforeAll {
        Import-Module $PSScriptRoot/Mailer.psm1 -Force
    }

    It 'sends exactly once and nothing leaves the building' {
        Mock Send-MailMessage { } -ModuleName Mailer

        Send-Welcome -To 'new@example.com' | Should -Be 'sent'

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

-ModuleName Mailer places the mock where the call actually resolves. Note the module name is the module itself (Mailer), not the .psm1 filename. Use -ModuleName on Should -Invoke too, so the verification counts calls in the same scope.

InModuleScope: running test code inside the module

-ModuleName is enough for mocking exported functions. But sometimes you need to reach inside the module — to call a private, non-exported function directly. That is what InModuleScope does: it runs a block of test code as if it were part of the module.

Add a private helper to Mailer.psm1 that is deliberately not exported:

function Format-Recipient {           # private: no Export-ModuleMember
    param([string]$Name)
    return $Name.Trim().ToLower()
}

You cannot call Format-Recipient from a normal test — it is invisible outside the module. InModuleScope lets you in:

Describe 'Format-Recipient (private)' {
    BeforeAll {
        Import-Module $PSScriptRoot/Mailer.psm1 -Force
    }

    It 'trims and lowercases the name' {
        InModuleScope Mailer {
            Format-Recipient -Name '  Alice  ' | Should -Be 'alice'
        }
    }
}

Inside the InModuleScope Mailer { ... } block, the module's private functions are in scope, and any Mock you define there lands in module scope automatically — so you could mock Send-MailMessage plainly inside the block without -ModuleName.

Rule of thumb: reach for -ModuleName to mock a command a module calls; reach for InModuleScope when you must call into the module's private internals.

Over-mocking is a design smell

The fact that you can mock everything does not mean you should. A suite where every dependency is faked has two problems:

  • It tests the mocks, not the code. If you stub out so much that nothing real runs, a green test proves only that you configured the stubs correctly.
  • It is brittle. Mocks are coupled to how the code calls its dependencies. Rename a parameter or refactor an internal helper, and a dozen over-specified mocks break — even though behavior is unchanged. Tests should break when behavior breaks, not when implementation shuffles.

If a single function needs five mocks to test, that is often the code talking: it is doing too much. Splitting the pure logic away from the side effects usually shrinks the mocking down to one or two true edges — a design improvement, not just a testing convenience.

When a real integration test is better

Mocks isolate one unit. They cannot tell you whether your code and the real dependency agree. A mocked Invoke-RestMethod returns the JSON you imagined the API sends, not what it really sends. So keep some tests that exercise the real thing:

  • Use unit tests with mocks for logic and branching — fast, many, run on every save.
  • Use a few integration tests (tagged Integration, per Part 16) that hit a real test database, a sandbox API, or a file under TestDrive: — slower, fewer, run before merge.

Over-mocking happens when you try to make unit tests do an integration test's job.

Try it yourself

Take the broken Send-Welcome test above (or a mock of your own that "isn't working" against one of your modules). Confirm it fails because the real command runs, then fix it by adding -ModuleName <YourModule> to both the Mock and the Should -Invoke. As a bonus, add a private helper to your module and call it through InModuleScope.

Common mistakes

Mocking in test scope when the call is in module scope. The mock is defined where the test runs, but the command resolves inside the module, so it never applies. Add -ModuleName, or work inside InModuleScope.

Using InModuleScope as a crutch for bad design. If you only reach into module internals because everything is private and entangled, the smell is the design, not the test. Export a seam or split the function before burrowing in.

Brittle, over-mocked suites. Tests that break on every refactor because they pin down internal call details are testing implementation, not behavior. Mock the true external edges and let real logic run.

Recap

A mock applies only in its own scope, so a command called inside a module needs -ModuleName <Module> on both Mock and Should -Invoke. Use InModuleScope when you must call a module's private functions directly. And remember that mocking is a scalpel, not a sledgehammer: mock the external edges, keep your logic real, and back unit tests with a few honest integration tests.

Next up: Part 22 — Measuring Code Coverage with Pester, where we find out which lines your tests actually exercise — and which risky branches you have been skipping.