Getting Your Function Under Test (Dot-Sourcing vs Modules)
- powershell
- pester
- testing
- dot-sourcing
- modules
- beforeall
📦 Run it yourself: download the companion code pack; this part's examples are in the
04-testing-real-code/folder (Pester 5-verified).
Up to now most of your tests have defined tiny functions inside a BeforeAll 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
.ps1file into a test withBeforeAll - 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
InModuleScopefor 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. It's easy to miss and 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 once, during Discovery, before any test runs, and dot-sourcing there can silently fail to make your function available when the tests 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.)
# Fails on purpose: don't do this. The dot-source 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!' # fails: Get-Greeting is not recognized
}
}Windows PowerShell 5.1 also prints a Suggestion [3,General] line saying the command exists in the current location. That's PowerShell's own hint; the real problem is when the dot-source ran.
Pattern 2: importing a module
When your code ships as a module (a .psm1 plus a .psd1 manifest), you don't dot-source it; you import it. Import-Module brings the module's exported functions into scope.
Here's a tiny module to practice on. Save it in a MyTools folder next to your test file. It exports Get-Greeting and keeps a helper, Format-Internal, to itself:
# MyTools/MyTools.psm1
function Get-Greeting {
param([Parameter(Mandatory)][string]$Name)
"Hello, $Name!"
}
function Format-Internal {
param([Parameter(Mandatory)][string]$Value)
$Value.PadLeft(4, '0')
}
Export-ModuleMember -Function 'Get-Greeting'And its manifest, which lists only Get-Greeting as exported:
# MyTools/MyTools.psd1
@{
RootModule = 'MyTools.psm1'
ModuleVersion = '1.0.0'
FunctionsToExport = @('Get-Greeting')
}The test imports the module by its manifest path:
# 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
# LoadBothWays.Tests.ps1
Describe 'Loading a loose script' {
BeforeAll {
. "$PSScriptRoot/Get-Greeting.ps1"
}
It 'finds Get-Greeting' {
Get-Greeting -Name 'Ada' | Should -Be 'Hello, Ada!'
}
}
Describe 'Importing a module' {
BeforeAll {
Import-Module "$PSScriptRoot/MyTools/MyTools.psd1" -Force
}
It 'finds Get-Greeting' {
Get-Greeting -Name 'Ada' | Should -Be 'Hello, Ada!'
}
}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 fails with a CommandNotFoundException.
The fix is InModuleScope, which runs a block inside the module so it can see the private members:
# MyTools.Private.Tests.ps1
BeforeAll {
Import-Module "$PSScriptRoot/MyTools/MyTools.psd1" -Force
}
Describe 'Format-Internal (private)' {
It 'pads to four characters' {
InModuleScope MyTools {
Format-Internal '7' | Should -Be '0007'
}
}
}Reach for this only when you 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
BeforeAllruns in the Discovery phase. Move every.andImport-ModuleintoBeforeAllso it runs when the tests execute.Stale module in memory. Import without
-Forceand PowerShell may keep an old copy loaded, so your edits never take effect. AlwaysImport-Module ... -Forcein 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 go inside BeforeAll, and both use $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.
Posts here are written with AI assistance and tested against real code before publishing.