Simon Elms
Simon Elms

Reputation: 19748

How to mock a command called twice with different parameters and different results

I have a PowerShell function I want to test with Pester:

function Install-RequiredModule (
    [string]$ModuleName,
    [string]$RepositoryName,
    [string]$ProxyUrl
    )
{
    # Errors from Install-Module are non-terminating.  They won't be caught using  
    # try - catch.  So check $Error instead.
    # Clear errors so we know if one shows up it must have been due to Install-Module.
    $Error.Clear()

    # Want to fail silently, without displaying anything in console to scare the user, 
    # because it's valid for Install-Module to fail for a user behind a proxy server.
    Install-Module -Name $ModuleName -Repository $RepositoryName `
        -ErrorAction SilentlyContinue -WarningAction SilentlyContinue

    if ($Error.Count -eq 0)
    {
        # throw 'NO error'
        return
    }

    # There was an error so try again, this time with proxy details.

    $proxyCredential = Get-Credential -Message 'Please enter credentials for proxy server'

    # No need to Silently Continue this time.  We want to see the error details.
    $Error.Clear()

    Install-Module -Name $ModuleName -Repository $RepositoryName `
        -Proxy $ProxyUrl -ProxyCredential $proxyCredential

    if ($Error.Count -gt 0)
    {
        throw $Error[0]
    }

    if (-not (Get-InstalledModule -Name $ModuleName -ErrorAction SilentlyContinue))
    {
        throw "Unknown error installing module '$ModuleName' from repository '$RepositoryName'."
    }

    Write-Output "Module '$ModuleName' successfully installed from repository '$RepositoryName'."
}

This function can call Install-Module twice. It first tries without proxy credentials, as if it has direct access to the internet. If that fails it tries again, this time with proxy credentials.

How can I test this functionality with Pester?

I read in the PowerShell forums, here, that I should be a able to mock the same command twice with different parameter filters. So this is what I tried:

function ExecuteInstallRequiredModule ()
{
    Install-RequiredModule -ModuleName 'TestModule' -RepositoryName 'TestRepo' `
        -ProxyUrl 'http://myproxy'
}

Describe 'Install-RequiredModule' {

    $securePassword = "mypassword" | 
        ConvertTo-SecureString -asPlainText -Force
    $psCredential = New-Object System.Management.Automation.PSCredential  ('MyUserName', $securePassword)
    Mock Get-Credential { return $psCredential }

    # Want to add an error to $Error without it being written to the host.
    Mock Install-Module { Write-Error "Some error" -ErrorAction SilentlyContinue } `
        -ParameterFilter { $Name -eq  'TestModule' -and $Repository -eq 'TestRepo' -and $ErrorAction -eq 'SilentlyContinue' -and $WarningAction -eq 'SilentlyContinue'}
    Mock Install-Module { return $Null } `
        -ParameterFilter { $Name -eq  'TestModule' -and $Repository -eq 'TestRepo' -and $Proxy -eq 'http://myproxy' -and $ProxyCredential -eq $psCredential }

    Mock Get-InstalledModule { return @('Non-null text') }

    It 'attempts to install module a second time if first attempt fails' {
        ExecuteInstallRequiredModule
        #Assert-VerifiableMock
        #Assert-MockCalled Install-Module -Scope It -Times 2
    }
}

Uncommenting the line in the function under test that says # throw 'NO error' I find that the $Error.Count is 0 after the first call to Install-Module. So the mock that is creating a non-terminating error is not being called and the function returns before the second call to Install-Module.

Upvotes: 1

Views: 1592

Answers (3)

boxdog
boxdog

Reputation: 8442

The problem seems to be that Pester blocks filtering on common parameters, so your use of 'ErrorAction', etc is causing your filter to fail.

You can see the parameters being removed from mocked functions at around line 254 in the Pester mock code: Mock.ps1

And also, testing for this removal is one of Pester's own unit tests (line 283): Mock.tests.ps1

Upvotes: 2

Simon Elms
Simon Elms

Reputation: 19748

For anyone in a similar situation, here's the final version of the test that worked:

function Install-RequiredModule (
    [string]$ModuleName,
    [string]$RepositoryName,
    [string]$ProxyUrl
    )
{
    try
    {
        Install-Module -Name $ModuleName -Repository $RepositoryName `
            -ErrorAction Stop

        return
    }
    catch {}

    # There was an error so try again, this time with proxy details.

    $proxyCredential = Get-Credential -Message 'Please enter credentials for proxy server'

    # No need to Silently Continue this time.  We want to see the error details.
    $Error.Clear()

    Install-Module -Name $ModuleName -Repository $RepositoryName `
        -Proxy $ProxyUrl -ProxyCredential $proxyCredential

    if ($Error.Count -gt 0)
    {
        throw $Error[0]
    }

    if (-not (Get-InstalledModule -Name $ModuleName -ErrorAction SilentlyContinue))
    {
        throw "Unknown error installing module '$ModuleName' from repository '$RepositoryName'."
    }

    Write-Output "Module '$ModuleName' successfully installed from repository '$RepositoryName'."
}

#region Tests *************************************************************************************

function ExecuteInstallRequiredModule ()
{
    Install-RequiredModule -ModuleName 'TestModule' -RepositoryName 'TestRepo' `
        -ProxyUrl 'http://myproxy'
}

Describe 'Install-RequiredModule' {

    $securePassword = "mypassword" | 
        ConvertTo-SecureString -asPlainText -Force
    $psCredential = New-Object System.Management.Automation.PSCredential  ('MyUserName', $securePassword)
    Mock Get-Credential { return $psCredential }

    Mock Install-Module { Write-Error "Some error" }
    Mock Install-Module { return $Null } -ParameterFilter { $Proxy -ne $Null -and $ProxyCredential -ne $Null }

    Mock Get-InstalledModule { return @('Non-null text') }

    It 'attempts to install module a second time, with proxy, if first attempt fails' {
        ExecuteInstallRequiredModule  
        Assert-MockCalled Install-Module -Scope It -Times 2 -Exactly
    }
}

#endregion

Upvotes: 0

Sid
Sid

Reputation: 2676

You can call the install-module command with -ErrorAction Stop inside a try catch loop.

try
{
    #run with no credentials
    Install-Module -Name $ModuleName -Repository $RepositoryName -ErrorAction stop -WarningAction SilentlyContinue
}
catch
{
    #when fails, run with proxy credentials
    Install-Module -Name $ModuleName -Repository $RepositoryName -Proxy $ProxyUrl -ProxyCredential $proxyCredential
}

You can have multiple catch{} block for the same try command that would catch different types of failures and execute a script block of your choice.

Upvotes: 1

Related Questions