diff --git a/src/functions/InModuleScope.ps1 b/src/functions/InModuleScope.ps1 index 83e5027c8..9b9066b43 100644 --- a/src/functions/InModuleScope.ps1 +++ b/src/functions/InModuleScope.ps1 @@ -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. @@ -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. @@ -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 = { @@ -165,7 +165,7 @@ & $wrapper $splat } -function Get-ScriptModule { +function Get-CompatibleModule { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] @@ -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] } diff --git a/src/functions/Mock.ps1 b/src/functions/Mock.ps1 index 2b6f984b5..0fbc4d7c0 100644 --- a/src/functions/Mock.ps1 +++ b/src/functions/Mock.ps1 @@ -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)." } @@ -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 @@ -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 } } diff --git a/src/functions/Pester.SessionState.Mock.ps1 b/src/functions/Pester.SessionState.Mock.ps1 index 3307669f9..91a611932 100644 --- a/src/functions/Pester.SessionState.Mock.ps1 +++ b/src/functions/Pester.SessionState.Mock.ps1 @@ -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. diff --git a/tst/functions/InModuleScope.Tests.ps1 b/tst/functions/InModuleScope.Tests.ps1 index fcee127d8..9f3f74fd1 100644 --- a/tst/functions/InModuleScope.Tests.ps1 +++ b/tst/functions/InModuleScope.Tests.ps1 @@ -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." + } } } @@ -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' + } +} \ No newline at end of file diff --git a/tst/functions/Mock.Tests.ps1 b/tst/functions/Mock.Tests.ps1 index 1e18428bb..ec9a6e341 100644 --- a/tst/functions/Mock.Tests.ps1 +++ b/tst/functions/Mock.Tests.ps1 @@ -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 + } +}