Search code examples
unit-testingpowershellpester

How to use Pester to mock functions in anonymous advanced functions


I recently discovered that I don't need to use Import-Module to use my advanced powershell functions, I can just define a function anonymously in a ps1 file.

Unfortunately, my Pester unit tests are broken. I can't seem to mock the call to New-Object in the listing below anymore. Typically, I would dot source the below code and have the function Get-StockQuote defined in my scope. Now dot sourcing the ps1 file does not help because I invoke the function via the file name anyway.

How can I use Pester to test the below code with a mock implementation of New-Object?

Note: This code is obviously trivial for the purpose of the question, the tests for code I am working with really does need a mock implementation of New-Object.

# Source listing of the file: Get-StockQuote.ps1
<#
.Synopsis
Looks up a stock quote
.Description
Uses the yahoo api to retrieve a recent quote for a given stock.
.Parameter Symbol
The stock trading symbol
.Example
Get-StockQuote.ps1 -Symbol AMZN
Prints the following line to the output
440.84
#>
[CmdletBinding()]
Param(
    [parameter(Mandatory=$false)]
    [string]$Symbol
)
BEGIN {
    Set-StrictMode -Version 1
}
PROCESS {
    (New-Object System.Net.WebClient).DownloadString("http://finance.yahoo.com/d/quotes.csv?s=$Symbol&f=l1")
}
END {
}

Solution

  • So I found a way to do this by defining a named function by the same name as the file name in the BEGIN block and invoking it from the PROCESS block.

    [CmdletBinding()]
    Param(
        [parameter(Mandatory=$false)]
        [string]$Symbol
    )
    
     BEGIN {
        Set-StrictMode -Version 1
        Function Get-StockQuote {
            [CmdletBinding()]
            Param(
                [parameter(Mandatory=$false)]
                [string]$Symbol
            )
            BEGIN{}
            PROCESS{
                (New-Object System.Net.WebClient).DownloadString("http://finance.yahoo.com/d/quotes.csv?s=$Symbol&f=l1")
            }
            END{}
        }
     }
     PROCESS {
        Get-StockQuote @PSBoundParameters
     }
     END {
     }
    

    This way, after dot sourcing my ps1 file, I will have the function definition in scope and Pester will start working appropriately.

    $here = Split-Path -Parent $MyInvocation.MyCommand.Path
    $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
    
    . "$here\$sut"
    
    Describe "Get a stock quote" {
        Mock New-Object {
            $retval = [pscustomobject]@{}
            Add-Member -InputObject $retval -MemberType ScriptMethod DownloadString {
                param( [string] $url )
                if ($url -imatch 'AMZN') {
                    return 500.01
                }
                return 100.00
            }
            return $retval
        } -ParameterFilter {$TypeName -and ($TypeName -ilike 'System.Net.WebClient') }
        Context "when called for AMZN" {
            $result = Get-StockQuote -Symbol AMZN
            It "Should RETURN 500.01" {
                $result | should be 500.01
            }
        }
        Context "when called for anything else" {
            $result = Get-StockQuote -Symbol MSFT
            It "Should RETURN 100.00" {
                $result | should be 100.00
            }
        }
    }