Skip to main content
Version: v5

Unit Testing within Modules

Let's say you have code like this inside a script module (.psm1 file):

MyModule.psm1
function BuildIfChanged {
$thisVersion = Get-Version
$nextVersion = Get-NextVersion
if ($thisVersion -ne $nextVersion) { Build $nextVersion }
return $nextVersion
}

function Build ($version) {
Write-Host "a build was run for version: $version"
}

# Actual definitions of Get-Version and Get-NextVersion are not shown here,
# since we'll just be mocking them anyway. However, the commands do need to
# exist in order to be mocked, so we'll stick dummy functions here

function Get-Version { return 0 }
function Get-NextVersion { return 0 }

Export-ModuleMember -Function BuildIfChanged

You wish to write a unit test for this module which mocks the calls to Get-Version and Get-NextVersion from the module's BuildIfChanged command. In older versions of Pester, this was not possible. As of version 3.0, there are two ways you can perform unit tests of PowerShell script modules. The first is to inject mocks into a module:

For these example, we'll assume that the PSM1 file is named MyModule.psm1, and that it is installed on your PSModulePath.

BeforeAll {
Import-Module MyModule
}

Describe "BuildIfChanged" {
Context "When there are Changes" {

BeforeAll {
Mock -ModuleName MyModule Get-Version { return 1.1 }
Mock -ModuleName MyModule Get-NextVersion { return 1.2 }

# Just for giggles, we'll also mock Write-Host here, to demonstrate that you can
# mock calls to commands other than functions defined within the same module.
Mock -ModuleName MyModule Write-Host {} -Verifiable -ParameterFilter {
$Object -eq 'a build was run for version: 1.2'
}

$result = BuildIfChanged
}

It "Builds the next version and calls Write-Host" {
Should -InvokeVerifiable
}

It "returns the next version number" {
$result | Should -Be 1.2
}
}

Context "When there are no Changes" {

BeforeAll {
Mock -ModuleName MyModule Get-Version { return 1.1 }
Mock -ModuleName MyModule Get-NextVersion { return 1.1 }
Mock -ModuleName MyModule Build { }

$result = BuildIfChanged
}

It "Should not build the next version" {
# -Scope Context is used below since BuildIfChanged is called in BeforeAll
# It's not required when the mock is called inside It
Should -Invoke Build -ModuleName MyModule -Times 0 -Scope Context -ParameterFilter {
$version -eq 1.1
}
}
}
}

Notice that in this example test script, all calls to Mock and Should -Invoke have had the -ModuleName MyModule parameter added. This tells Pester to inject the mock into the module's scope, which causes any calls to those commands from inside the module to execute the mock instead.

When you write your test script this way, you can mock commands that are called by the module's internal functions. However, your test script is still limited to accessing the public, exported members of the module. If you wanted to write a unit test that calls Build directly, for example, it wouldn't work using the above technique. That's where the second approach to script module testing comes into play. With Pester's InModuleScope command, you can cause entire sections of your test script to execute inside the targeted script module. This gives you access to non-exported members of the module. For example:

BeforeAll {
Import-Module MyModule
}

Describe "Unit testing the module's internal Build function:" {

It 'Outputs the correct message' {
InModuleScope MyModule {
$testVersion = 5.0
Mock Write-Host { }

Build $testVersion

Should -Invoke Write-Host -ParameterFilter {
$Object -eq "a build was run for version: $testVersion"
}
}
}
}

Notice that when using InModuleScope, you no longer need to specify a -ModuleName parameter when calling Mock or Should -Invoke for commands within that module. You are also able to directly call the Build function, which the module does not export.

Avoid putting in InModuleScope around your Describe and It blocks.

InModuleScope is a simple way to expose your internal module functions to be tested, but it prevents you from properly testing your published functions, does not ensure that your functions are actually published and slows down test discovery by loading the module. Aim to avoid it altogether by using -ModuleName on Mock when possible or at least limit it to inside the It block like the sample above.

Variables and functions created inside InModuleScope won't persist by default

The scriptblock provided to InModuleScope is executed in a local scope inside the module session state. If you're creating variables, functions etc. intended to be reused in later outside of this scriptblock, use the script: scope-modifier to make them available to all future scopes inside the module.