Skip to content

Commit

Permalink
Updates to Modules-page (#230)
Browse files Browse the repository at this point in the history
* update modules-page with module types limitations

* add public mock example and update text

* use highlighted line in codeblock

* remove style for outdated highlight-class

* update code highlight background

* Apply suggestions from code review
  • Loading branch information
fflaten authored Mar 30, 2024
1 parent fa48377 commit b51101d
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 13 deletions.
180 changes: 175 additions & 5 deletions docs/usage/modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ sidebar_label: Modules
description: PowerShell code in modules run in their own module state. This requires special attention when writing tests for internal functions or when mocking dependencies used by code in a module
---

## Introduction

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

```powershell title="MyModule.psm1"
Expand All @@ -29,9 +31,10 @@ 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 that the PSM1 file is named `MyModule.psm1`, and that it is installed on your PSModulePath.
For these example, we'll assume the module above is installed in a path defined in [`$env:PSModulePath`](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_psmodulepath).

```powershell
BeforeAll {
Expand All @@ -40,7 +43,6 @@ BeforeAll {
Describe "BuildIfChanged" {
Context "When there are Changes" {
BeforeAll {
Mock -ModuleName MyModule Get-Version { return 1.1 }
Mock -ModuleName MyModule Get-NextVersion { return 1.2 }
Expand All @@ -64,7 +66,6 @@ Describe "BuildIfChanged" {
}
Context "When there are no Changes" {
BeforeAll {
Mock -ModuleName MyModule Get-Version { return 1.1 }
Mock -ModuleName MyModule Get-NextVersion { return 1.1 }
Expand All @@ -84,17 +85,22 @@ Describe "BuildIfChanged" {
}
```

### -ModuleName

Notice that in this example test script, all calls to [Mock](../commands/Mock) and [Should -Invoke](../commands/Should) 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:
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](../commands/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:

```powershell
BeforeAll {
Import-Module MyModule
}
Describe "Unit testing the module's internal Build function:" {
It 'Outputs the correct message' {
InModuleScope MyModule {
$testVersion = 5.0
Expand All @@ -121,3 +127,167 @@ Notice that when using `InModuleScope`, you no longer need to specify a `-Module

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.

```powershell
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](https://docs.microsoft.com/en-us/powershell/scripting/developer/module/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:

```powershell
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`:

```powershell
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:

```powershell
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`.

```powershell
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
```

:::tip 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.

```powershell title="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:

```powershell title="MyModule.psd1"
@{
ModuleVersion = '1.0'
# highlight-next-line
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.
10 changes: 2 additions & 8 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
--ifm-font-family-base: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--ifm-heading-font-weight: 600;
--ifm-hover-overlay: rgba(0, 0, 0, 0.04);
--docusaurus-highlighted-code-line-bg: rgb(0, 0, 0, .1);
}

[data-theme='dark'] {
Expand All @@ -94,6 +95,7 @@
--ifm-breadcrumb-color-active: var(--ifm-color-primary-lightest);
--ifm-link-color: var(--ifm-color-primary-lightest);
--ifm-menu-color-active: var(--ifm-color-primary-lightest);
--docusaurus-highlighted-code-line-bg: rgb(255, 255, 255, .1);
}

[data-theme='dark'] .table-of-contents__link:hover,
Expand All @@ -116,14 +118,6 @@
background: rgb(255, 255, 255, 0.2);
}

/* highlighted lines in codeblocks. Example: highlight line 3 using ```powershell {3} */
.docusaurus-highlight-code-line {
background-color: rgb(72, 77, 91);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}

/* navbar styling */
.menu__link {
font-weight: var(--ifm-font-weight-normal);
Expand Down

0 comments on commit b51101d

Please sign in to comment.