2

Considering the below Powershell code, is there a way to mock $host.ui.PromptForChoice without the internalMenuWrapper function?

<#
    .Synopsis
        wrap the menu so we can mock calls to it
#>
function internalMenuWrapper {
    param (
        [Parameter(Mandatory=$true)]
        $prompt,
        [Parameter(Mandatory=$true)]
        $options
    )
    return = $host.ui.PromptForChoice("Waiting for user input", $prompt, [System.Management.Automation.Host.ChoiceDescription[]]$options, 0)
}

<#
    .Synopsis
        Create a menu with an array of choices and return the result
#>
function Invoke-Menu($prompt, $opts) {
        $options = @()
        foreach ($opt in $opts) {
                $options += $(new-object System.Management.Automation.Host.ChoiceDescription $opt)
        }
        $index = internalMenuWrapper $prompt $options
        $opts[$index]
}

Describe 'Invoke-Menu' {
    Context "when called" {
        It "returns the object that was selected" {
            #mock fails
            Mock internalMenuWrapper { return 0 }
            $result = Invoke-Menu "test menu" @("pass", "fail")
            $result | Should -Be "pass"
        }
    }
}
Dave Neeley
  • 3,526
  • 1
  • 24
  • 42

1 Answers1

3

As Mike Shepard points out in a comment, mocking methods isn't supported in Pester, only commands can be mocked (cmdlets, functions, aliases, external programs).

You can work around the issue by using the Get-Host cmdlet instead of $host and mock that:

function Invoke-Menu($prompt, $choices) {

  $choiceObjects = [System.Management.Automation.Host.ChoiceDescription[]] $choices

  # Use Get-Host instead of $host here; $host cannot be mocked, but Get-Host can.
  $index = (Get-Host).ui.PromptForChoice("Waiting for user input", $prompt, $choiceObjects, 0)

  $choices[$index]

}

Describe 'Invoke-Menu' {
  Context "when called" {
      It "returns the object that was selected" {
        # Mock Get-Host with a dummy .ui.PromptForChoice() method that instantly
        # returns 0 (the first choice).
        Mock Get-Host { 
          [pscustomobject] @{ 
            ui = Add-Member -PassThru -Name PromptForChoice -InputObject ([pscustomobject] @{}) -Type ScriptMethod -Value { return 0 }
          }          
        }
        Invoke-Menu 'test menu'  '&pass', '&fail' | Should -Be '&pass'
      }
  }
}

As you point out on GitHub, if the suggested Read-Choice cmdlet is ever implemented (as a PS-friendly wrapper around $host.ui.PromptForChoice()), it could be mocked directly (and there would be no custom code to test).

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    @DaveNeeley: Also note the simplified construction of the choice objects by cast, and the omission of `@(...)` around array literals. With the direct array cast it is unnecessary, but if you do need a `foreach` loop, note that you can treat it as an expression _as a whole_ that implicitly outputs an array: `[array] $options = foreach ($opt in $opts) { new-object System.Management.Automation.Host.ChoiceDescription $opt }` – mklement0 Aug 29 '19 at 19:43