Unit Testing within Modules
Introduction
Let's say you have code like this inside a script module (.psm1 file):
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.
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.
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.
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
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.
@{
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:
@{
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.