Skip to main content
Version: v5

Unit Testing within Modules

Introduction

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

Testing public functions

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 the module above is installed in a path defined in $env: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
}
}
}
}

-ModuleName

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.

Testing private functions

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.

Working with different module types

Pester supports most module types in PowerShell, but there are some limitations with Mock and InModuleScope features for some module types.

Types of Modules

PowerShell modules are a way of grouping related scripts and resources together to make it easier to use them. There are a number of different types of modules, each of which have slightly different characteristics:

  • Script modules
  • Binary modules
  • Manifest modules
  • Dynamic modules (will also return Script as ModuleType)

To determine the type of a module you can use the Get-Module cmdlet.

ModuleType Version    Name
---------- ------- ----
Script 0.0 __DynamicModule_11b8a091-bd9b-49...
Binary 1.0.0.0 CimCmdlets
Manifest 3.1.0.0 Microsoft.PowerShell.Management
Manifest 3.1.0.0 Microsoft.PowerShell.Utility
Script 5.3.3 Pester
Script 2.0.0 PSReadline

To inspect your modules you might need to use -ListAvailable or load the module first using Import-Module and then inspect it.

tip

Read more about the different module types at Microsoft Docs, see Understanding a Windows PowerShell Module.

Usage and workarounds

Pester can be used to both test and mock the behavior commands that are exported from all types of modules. For example the following tests will call the real Invoke-PublicMethod command and call a mocked implementation of it regardless of whether it is defined in a Script, Binary, Manifest or Dynamic module:

Describe "Invoke-PublicMethod" {
It "returns a value" {
$result = Invoke-PublicMethod
$result | Should Be 'Invoke-PublicMethod called!'
}

It "mocking exported command" {
Mock Invoke-PublicMethod { 'mocked' }
$result = Invoke-PublicMethod
$result | Should Be 'mocked'
}
}

However injecting mocks into or executing code inside a Binary module is not possible due to how they are implemented in PowerShell. As a result, you may see an error message when trying to use Mock -ModuleName or InModuleScope:

Module 'MyBinaryModule' is not a Script or Manifest module. Detected modules of the following types: 'Binary'

The following sections describe Pester's support for the Mock and InModuleScope features for each type of module and workarounds if available.

Script Modules

Pester fully supports Script modules, so both Mock and InModuleScope can be used without any workarounds.

Dynamic Modules

The Mock and InModuleScope features can be used with Dynamic modules if the module is first imported using Import-Module. Example:

BeforeAll {
# create a dynamic module
$myDynamicModule = New-Module -Name MyDynamicModule {
function Invoke-PrivateFunction { 'I am the internal function' }
function Invoke-PublicFunction { Invoke-PrivateFunction }
Export-ModuleMember -Function Invoke-PublicFunction
}

# remove previously imported (to enable rerunning the tests)
Get-Module MyDynamicModule -ErrorAction SilentlyContinue | Remove-Module

# import the dynamic module
$myDynamicModule | Import-Module -Force
}

# use InModuleScope and Mock for commands inside the dynamic module
Describe 'Executing test code inside a dynamic module' {
Context 'Using the Mock command' {
It 'Can mock functions inside the module when using Mock -ModuleName' {
Mock Invoke-PrivateFunction -ModuleName MyDynamicModule -MockWith { 'I am the mock function.' }
Invoke-PublicFunction | Should -Be 'I am the mock function.'
Should -Invoke Invoke-PrivateFunction -ModuleName MyDynamicModule
}
}

It 'Can call module internal functions using InModuleScope' {
InModuleScope MyDynamicModule {
Invoke-PrivateFunction | Should -Be 'I am the internal function'
}
}
It 'Can mock functions inside the module without using Mock -ModuleName when inside InModuleScope' {
InModuleScope MyDynamicModule {
Mock Invoke-PrivateFunction -MockWith { 'I am the mock function.' }
Invoke-PrivateFunction | Should -Be 'I am the mock function.'
Should -Invoke Invoke-PrivateFunction
}
}
}

Manifest Modules

Pester 5.4 and later fully supports Manifest modules, so both Mock and InModuleScope can be used without any workarounds. For earlier versions, see workaround below.

Be aware that only code in nested scripts (*.ps1) execute directly from the manifest module. Nested script modules (*.psm1) or binary modules (*.dll) are executed in their own module state. In the example below, mocking calls made inside Get-HelloWorld would require -ModuleName MyNestedModule because it's was defined in MyNestedModule.psm1.

Get-Command Get-HelloWorld

CommandType Name Version Source
----------- ---- ------- ------
Function Get-HelloWorld 0.0.1 MyManifestModule

(Get-Module MyManifestModule).NestedModules

ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 0.0 MyNestedModule Get-HelloWorld

Get-HelloWorld
Hello World from module: MyNestedModule
Workaround for older versions of Pester

Prior to Pester 5.4, only exported members from a manifest module could be tested with Pester and the Mock and InModuleScope features were unavailable. However, by creating a empty script module with *.psm1 extension and adding it into the RootModule (or ModuleToProcess) attribute of the manifest *.psd1 file, the module is converted to a Script module.

For example, save the manifest below to create a PowerShell Manifest module.

MyModule.psd1
@{
ModuleVersion = '1.0'
NestedModules = @( "Invoke-PrivateManifestMethod.ps1", "Invoke-PublicManifestMethod.ps1" )
FunctionsToExport = @( "Invoke-PublicManifestMethod" )
}

To convert it into a Script module, create a new blank file called MyModule.psm1 and modify the manifest created above as follows:

MyModule.psd1
@{
ModuleVersion = '1.0'
RootModule = "MyModule.psm1"
NestedModules = @( "Invoke-PrivateManifestMethod.ps1", "Invoke-PublicManifestMethod.ps1" )
FunctionsToExport = @( "Invoke-PublicManifestMethod" )
}

PowerShell will then load the module as a Script module and Pester's Mock and InModuleScope features will work as expected.

Binary Modules

Exported commands from a Binary module can be tested and mocked using with Mock for calls made in script or other modules.

Use of InModuleScope and injecting mocks inside module (using -ModuleName MyBinaryModule) are not possible and there are no workarounds.