Skip to content

Commit

Permalink
Support mocking and execution in manifest modules (pester#2234)
Browse files Browse the repository at this point in the history
  • Loading branch information
fflaten authored Oct 2, 2022
1 parent fb87da9 commit ffedd47
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 26 deletions.
26 changes: 13 additions & 13 deletions src/functions/InModuleScope.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<#
.SYNOPSIS
Allows you to execute parts of a test script within the
scope of a PowerShell script module.
scope of a PowerShell script or manifest module.
.DESCRIPTION
By injecting some test code into the scope of a PowerShell
script module, you can use non-exported functions, aliases
script or manifest module, you can use non-exported functions, aliases
and variables inside that module, to perform unit tests on
its internal implementation.
Expand All @@ -16,7 +16,7 @@
injected. This module must already be loaded into the current
PowerShell session.
.PARAMETER ScriptBlock
The code to be executed within the script module.
The code to be executed within the script or manifest module.
.PARAMETER Parameters
A optional hashtable of parameters to be passed to the scriptblock.
Parameters are automatically made available as variables in the scriptblock.
Expand Down Expand Up @@ -118,7 +118,7 @@
$ArgumentList = @()
)

$module = Get-ScriptModule -ModuleName $ModuleName -ErrorAction Stop
$module = Get-CompatibleModule -ModuleName $ModuleName -ErrorAction Stop
$sessionState = Set-SessionStateHint -PassThru -Hint "Module - $($module.Name)" -SessionState $module.SessionState

$wrapper = {
Expand Down Expand Up @@ -165,7 +165,7 @@
& $wrapper $splat
}

function Get-ScriptModule {
function Get-CompatibleModule {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
Expand All @@ -186,24 +186,24 @@ function Get-ScriptModule {
throw "No modules named '$ModuleName' are currently loaded."
}

$scriptModules = @($modules | & $SafeCommands['Where-Object'] { $_.ModuleType -eq 'Script' })
if ($scriptModules.Count -gt 1) {
throw "Multiple script modules named '$ModuleName' are currently loaded. Make sure to remove any extra copies of the module from your session before testing."
$compatibleModules = @($modules | & $SafeCommands['Where-Object'] { $_.ModuleType -in 'Script', 'Manifest' })
if ($compatibleModules.Count -gt 1) {
throw "Multiple script or manifest modules named '$ModuleName' are currently loaded. Make sure to remove any extra copies of the module from your session before testing."
}

if ($scriptModules.Count -eq 0) {
if ($compatibleModules.Count -eq 0) {
$actualTypes = @(
$modules |
& $SafeCommands['Where-Object'] { $_.ModuleType -ne 'Script' } |
& $SafeCommands['Where-Object'] { $_.ModuleType -notin 'Script', 'Manifest' } |
& $SafeCommands['Select-Object'] -ExpandProperty ModuleType -Unique
)

$actualTypes = $actualTypes -join ', '

throw "Module '$ModuleName' is not a Script module. Detected modules of the following types: '$actualTypes'"
throw "Module '$ModuleName' is not a Script or Manifest module. Detected modules of the following types: '$actualTypes'"
}
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Runtime "Found module $ModuleName version $($scriptModules[0].Version)."
Write-PesterDebugMessage -Scope Runtime "Found module $ModuleName version $($compatibleModules[0].Version)."
}
return $scriptModules[0]
return $compatibleModules[0]
}
6 changes: 3 additions & 3 deletions src/functions/Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ function Resolve-Command {
$SessionState = $callerSessionState
}
else {
$module = Get-ScriptModule -ModuleName $ModuleName -ErrorAction Stop
$module = Get-CompatibleModule -ModuleName $ModuleName -ErrorAction Stop
if ($PesterPreference.Debug.WriteDebugMessages.Value) {
Write-PesterDebugMessage -Scope Mock "Found module $($module.Name) version $($module.Version)."
}
Expand Down Expand Up @@ -1604,7 +1604,7 @@ function Remove-MockFunctionsAndAliases ($SessionState) {
Set-ScriptBlockScope -SessionState $SessionState -ScriptBlock $ScriptBlock
& $ScriptBlock $Get_Alias $Get_Command $Remove_Item

# clean up also in all loaded script modules
# clean up also in all loaded script and manifest modules
$modules = & $script:SafeCommands['Get-Module']
foreach ($module in $modules) {
# we cleaned up in module on the start of this method without overhead of moving to module scope
Expand All @@ -1615,7 +1615,7 @@ function Remove-MockFunctionsAndAliases ($SessionState) {
# some script modules aparently can have no session state
# https://github.com/PowerShell/PowerShell/blob/658837323599ab1c7a81fe66fcd43f7420e4402b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs#L51-L55
# https://github.com/pester/Pester/issues/1921
if ('Script' -eq $module.ModuleType -and $null -ne $module.SessionState) {
if ('Script', 'Manifest' -contains $module.ModuleType -and $null -ne $module.SessionState) {
& ($module) $ScriptBlock $Get_Alias $Get_Command $Remove_Item
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/functions/Pester.SessionState.Mock.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ function Mock {
verifiable mock is not called, Should -InvokeVerifiable will throw an
exception and indicate all mocks not called.
If you wish to mock commands that are called from inside a script module,
you can do so by using the -ModuleName parameter to the Mock command. This
injects the mock into the specified module. If you do not specify a
If you wish to mock commands that are called from inside a script or manifest
module, you can do so by using the -ModuleName parameter to the Mock command.
This injects the mock into the specified module. If you do not specify a
module name, the mock will be created in the same scope as the test script.
You may mock the same command multiple times, in different scopes, as needed.
Each module's mock maintains a separate call history and verified status.
Expand Down
94 changes: 87 additions & 7 deletions tst/functions/InModuleScope.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,62 @@ Describe "Executing test code inside a module" {
}
}

Describe "Get-ScriptModule behavior" {
Describe 'Get-CompatibleModule' {

Context 'when module name matches imported script module' {
It 'should return a single ModuleInfo object' {
$moduleInfo = InPesterModuleScope { Get-CompatibleModule -ModuleName Pester }
$moduleInfo | Should -Not -BeNullOrEmpty
@($moduleInfo).Count | Should -Be 1
$moduleInfo.Name | Should -Be 'Pester'
$moduleInfo.ModuleType | Should -Be 'Script'
}
}

Context 'when module name matches imported manifest module' {
BeforeAll {
$moduleName = 'testManifestModule'
$moduleManifestPath = "TestDrive:/$moduleName.psd1"
New-ModuleManifest -Path $moduleManifestPath
Import-Module $moduleManifestPath -Force
}

Context "When attempting to mock a command in a non-existent module" {
AfterAll {
Get-Module $moduleName -ErrorAction SilentlyContinue | Remove-Module
Remove-Item $moduleManifestPath -Force -ErrorAction SilentlyContinue
}

It 'should return a single ModuleInfo object' {
$moduleInfo = InPesterModuleScope { Get-CompatibleModule -ModuleName testManifestModule }
$moduleInfo | Should -Not -BeNullOrEmpty
@($moduleInfo).Count | Should -Be 1
$moduleInfo.Name | Should -Be 'testManifestModule'
$moduleInfo.ModuleType | Should -Be 'Manifest'
}
}

Context 'when module name does not resolve to imported module' {
It "should throw an exception" {
{
Mock -CommandName "Invoke-MyMethod" `
-ModuleName "MyNonExistentModule" `
-MockWith { write-host "my mock called!" }
} | Should -Throw "No modules named 'MyNonExistentModule' are currently loaded."
$sb = { InPesterModuleScope { Get-CompatibleModule -ModuleName MyNonExistentModule } }
$sb | Should -Throw "No modules named 'MyNonExistentModule' are currently loaded."
}
}

Context 'when module name matches multiple imported modules' {
BeforeAll {
Get-Module 'MyDuplicateModule' -ErrorAction SilentlyContinue | Remove-Module
New-Module -Name 'MyDuplicateModule' { } | Import-Module -Force
New-Module -Name 'MyDuplicateModule' { } | Import-Module -Force
}

AfterAll {
Get-Module 'MyDuplicateModule' -ErrorAction SilentlyContinue | Remove-Module
}

It "should throw an exception" {
$sb = { InPesterModuleScope { Get-CompatibleModule -ModuleName MyDuplicateModule } }
$sb | Should -Throw "Multiple script or manifest modules named 'MyDuplicateModule' are currently loaded. Make sure to remove any extra copies of the module from your session before testing."
}
}

}
Expand Down Expand Up @@ -261,3 +305,39 @@ Describe "Using variables within module scope" {
Remove-Module TestModule2 -Force
}
}

Describe 'Working with manifest modules' {
BeforeAll {
$moduleName = 'inManifestModule'
$moduleManifestPath = "TestDrive:/$moduleName.psd1"
$scriptPath = "TestDrive:/$moduleName-functions.ps1"

Set-Content -Path $scriptPath -Value {
function myPublicFunction {
myPrivateFunction
}

function myPrivateFunction {
'real'
}
}

New-ModuleManifest -Path $moduleManifestPath -NestedModules "$moduleName-functions.ps1" -FunctionsToExport 'myPublicFunction'
Import-Module $moduleManifestPath -Force
}

AfterAll {
Get-Module $moduleName -ErrorAction SilentlyContinue | Remove-Module
Remove-Item $moduleManifestPath, $scriptPath -Force -ErrorAction SilentlyContinue
}

It "Should invoke inside module's sessions state" {
$res = InModuleScope -ModuleName $moduleName -ScriptBlock { $ExecutionContext.SessionState.Module }
$res.Name | Should -Be $moduleName
}

It 'Should be able to invoke private functions' {
$res = InModuleScope -ModuleName $moduleName -ScriptBlock { myPrivateFunction }
$res | Should -Be 'real'
}
}
36 changes: 36 additions & 0 deletions tst/functions/Mock.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2972,3 +2972,39 @@ Describe "When inherited variables conflicts with parameters" {
Should -Invoke FunctionUnderTest -ParameterFilter { $param1 -eq 123 } -Times 1 -Exactly
}
}

Describe 'Mocking in manifest modules' {
BeforeAll {
$moduleName = 'MockManifestModule'
$moduleManifestPath = "TestDrive:/$moduleName.psd1"
$scriptPath = "TestDrive:/$moduleName-functions.ps1"
Set-Content -Path $scriptPath -Value {
function myManifestPublicFunction {
myManifestPrivateFunction
}

function myManifestPrivateFunction {
'real'
}
}
New-ModuleManifest -Path $moduleManifestPath -NestedModules "$moduleName-functions.ps1" -FunctionsToExport 'myManifestPublicFunction'
Import-Module $moduleManifestPath -Force
}

AfterAll {
Get-Module $moduleName -ErrorAction SilentlyContinue | Remove-Module
Remove-Item $moduleManifestPath, $scriptPath -Force -ErrorAction SilentlyContinue
}

It 'Should be able to mock public function' {
Mock -CommandName 'myManifestPublicFunction' -MockWith { 'mocked public' }
myManifestPublicFunction | Should -Be 'mocked public'
Should -Invoke -CommandName 'myManifestPublicFunction' -Exactly -Times 1
}

It 'Should be able to mock private function' {
Mock -CommandName 'myManifestPrivateFunction' -ModuleName $moduleName -MockWith { 'mocked private' }
myManifestPublicFunction | Should -Be 'mocked private'
Should -Invoke -CommandName 'myManifestPrivateFunction' -ModuleName $moduleName -Exactly -Times 1
}
}

0 comments on commit ffedd47

Please sign in to comment.