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
.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 — 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
BeforeAllruns in the Discovery phase. Move every.andImport-ModuleintoBeforeAllso it runs when the tests actually 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 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.