Skip to content
5 min read

Getting Your Function Under Test (Dot-Sourcing vs Modules)

  • powershell
  • pester
  • testing
  • dot-sourcing
  • modules
  • beforeall

Up to now your tests have used tiny functions defined right in the It block. That's fine for learning, but real life means loading your code—the function that lives in Get-Report.ps1 or inside your team's module. There are two clean ways to do it, and once you know both, you can test almost any PowerShell you already have.

What you'll learn

  • How to dot-source a .ps1 file into a test with BeforeAll
  • How to load a function that ships inside a module with Import-Module
  • Why the load belongs in BeforeAll, not at the top of the file
  • How to keep load paths robust with $PSScriptRoot
  • A first look at InModuleScope for private functions (full treatment in Part 21)

Pattern 1: dot-sourcing a script file

Dot-sourcing runs a script in the current scope, so any functions it defines stick around for your tests to call. Say you have a function in its own file:

# Get-Greeting.ps1
function Get-Greeting {
    param([Parameter(Mandatory)][string]$Name)
    "Hello, $Name!"
}

The test file sits beside it and dot-sources it inside BeforeAll:

# Get-Greeting.Tests.ps1
BeforeAll {
    . "$PSScriptRoot/Get-Greeting.ps1"
}

Describe 'Get-Greeting' {
    It 'greets the named person' {
        Get-Greeting -Name 'Ada' | Should -Be 'Hello, Ada!'
    }
}

The leading dot and a space (. "path") is the dot-source operator — easy to miss, easy to get wrong. Put both files in a folder, then run:

Invoke-Pester -Path ./Get-Greeting.Tests.ps1

$PSScriptRoot is the folder the test file lives in, so the path works no matter where you launch Pester from. That single habit saves you from a pile of "file not found" failures.

Why the load goes in BeforeAll

It is tempting to dot-source at the very top of the file, outside any block. Resist it. Pester 5 runs your file in two phases: Discovery (it reads the file to find every Describe/It) and Run (it executes the tests). Code at the top level runs during Discovery, on every block, often at a surprising time — and dot-sourcing there can silently fail to make your function available when the tests actually run.

BeforeAll runs in the Run phase, right before the tests in its scope. That is exactly when you want your function loaded. So the rule is simple: loading code is setup, and setup lives in BeforeAll. (Part 10 covers the phase model in depth.)

# Don't do this — runs in Discovery, not Run
. "$PSScriptRoot/Get-Greeting.ps1"

Describe 'Get-Greeting' {
    It 'greets the named person' {
        Get-Greeting -Name 'Ada' | Should -Be 'Hello, Ada!'   # may error: command not found
    }
}

Pattern 2: importing a module

When your code ships as a module (a .psm1 plus a .psd1 manifest), you do not dot-source it — you import it. Import-Module brings the module's exported functions into scope.

# MyTools.Tests.ps1
BeforeAll {
    Import-Module "$PSScriptRoot/MyTools/MyTools.psd1" -Force
}

Describe 'Get-Greeting (from module)' {
    It 'greets the named person' {
        Get-Greeting -Name 'Ada' | Should -Be 'Hello, Ada!'
    }
}

The -Force switch is the important part. Without it, if the module is already loaded from an earlier run, PowerShell keeps the old copy in memory and your code changes are ignored — you fix a bug, rerun, and the test still fails against stale code. -Force re-imports the current version every time. When in doubt, force the import.

The two patterns side by side

# Dot-sourcing a loose script:
BeforeAll {
    . "$PSScriptRoot/Get-Greeting.ps1"
}

# Importing a module:
BeforeAll {
    Import-Module "$PSScriptRoot/MyTools/MyTools.psd1" -Force
}

Rule of thumb: if the code is a standalone .ps1, dot-source it. If it is packaged as a module, import it. Both load into BeforeAll; both use $PSScriptRoot for the path.

A peek at private functions

Modules only expose what they Export-ModuleMember (or list in the manifest's FunctionsToExport). A helper function the module keeps to itself is not visible to your test, even after Import-Module. Calling it gives "command not found."

The fix is InModuleScope, which runs a block inside the module so it can see the private members:

Describe 'Format-Internal (private)' {
    It 'pads to four characters' {
        InModuleScope MyTools {
            Format-Internal '7' | Should -Be '0007'
        }
    }
}

Reach for this only when you genuinely need to test a non-exported function. Most of the time you test through the public surface. We give InModuleScope the full treatment in Part 21 — for now, just know the escape hatch exists.

Try it yourself

Pick one of your own script files that defines a function. Create a *.Tests.ps1 next to it. In BeforeAll, dot-source the file using $PSScriptRoot. Then write one It that calls the function and asserts on its output with Should -Be. Run it with Invoke-Pester and confirm it goes green. Now break the function on purpose and watch the test catch it.

Common mistakes

Dot-sourcing at the top level. Code outside BeforeAll runs in the Discovery phase. Move every . and Import-Module into BeforeAll so it runs when the tests actually execute.

Stale module in memory. Import without -Force and PowerShell may keep an old copy loaded, so your edits never take effect. Always Import-Module ... -Force in tests.

Testing private functions directly. Non-exported functions are invisible after a normal import. Wrap the call in InModuleScope ModuleName { ... } — or, better, test through the public function that uses it.

Recap

You can now load real code two ways: dot-source a loose .ps1, or Import-Module -Force a packaged module — both inside BeforeAll, both with $PSScriptRoot for a portable path. You also met InModuleScope for the rare private-function case. With your code loaded, the next question is what happens when that code writes files or touches the registry.

Next up: Part 14 — Safe File and Registry Tests with TestDrive and TestRegistry.