1

I came across a utility function: Get-ParameterValues, by @Jaykul, along with a slightly modified version by elovelan. I chose to use elovelan's version which doesn't declare parameters. I can confirm that, as far as I've tested at the command line, the function works as intended.

I'm incorporating the function into a private module that I'm writing as a general utility function (with due-attribution, of course—thanks @jaykul and elovelan!!), and as such, I am trying to write unit tests for it. However, when running the tests, I'm getting the error Cannot find a variable with the name 'PSBoundParameters'.

I have a feeling this is less to do with Pester and more to do with my understanding of PowerShell. I'm hoping someone here in the community may be able to help me.

The Pester test fails with the error Cannot find a variable with the name 'PSBoundParameters'.

The function under test can be found here. Here is a portion of the test code:

Feature file

Feature: Get-ParameterValues

  As a PowerShell function author
  In order to more easily deal with function parameters
  I want to be able to automatically populate a dictionary
  containing the supplied function arguments or the parameter's
  default value if the argument was not supplied
  So that I can reduce boilerplate code when dealing with
  function parameters.

  Background: A test function with parameters

    Given a test function
      """
      function global:Test {
          [CmdletBinding()]
          param(
              [Parameter(Position = 0)]
              $ValueWithNoDefault,
              [Parameter(Position = 1)]
              $ValueWithDefault = 'default',
              [Parameter(Position = 2)]
              $ValueWithNullDefault = $null
          )

          $Parameters = (Get-ParameterValues)
          $Parameters
      }
      """

  Scenario Outline: Get-ParameterValues returns a dictionary with the expected key and value

    Given the value '<Value>' is provided for the '<Parameter>' parameter
     When 'global:Test' is invoked
     Then the hashtable '<ContainsAKey>' a key for the parameter
      And the resolved parameter value is '<ExpectedValue>'.

    Examples: Examples using parameter ValueWithNoDefault
      | Parameter            | Value         | Contains         | ExpectedValue    |
      | ValueWithNoDefault   | <No value>    | does not contain | <Does Not Exist> |
      | ValueWithNoDefault   | `$null        | contains         | `$null           |
      | ValueWithNoDefault   | { `$foo = 1 } | contains         | { `$foo = 1 }    |
      | ValueWithNoDefault   | `$false       | contains         | `$false          |
      | ValueWithNoDefault   | `$true        | contains         | `$true           |
      | ValueWithNoDefault   | ""            | contains         | ""               |
      | ValueWithNoDefault   | 0             | contains         | 0                |
      | ValueWithNoDefault   | @()           | contains         | @()              |
      | ValueWithNoDefault   | 1             | contains         | 1                |

Step Definitions

BeforeEachFeature {
    Import-Module -Name "${pwd}\path-to\My-Module.psd1" -Scope Global -Force

    filter global:ConvertFrom-TableCellValue {
        Param(
            [Parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
            [AllowNull()]
            [AllowEmptyString()]
            $Value
        )

        Process {
            if ($Value -eq '$null') {
                $Value = $null
            }

            switch -Regex ($Value) {
                '^"?\{\s*(?<ScriptBody>.*)\s*\}"?$'   { $Value = [ScriptBlock]::Create($Matches.ScriptBody) }
                '(?<StringValue>".*")'                { $Value = ($Matches.StringValue -as [string]) }
                '$?(?i:(?<Boolean>true|false))'       { $Value = [Boolean]::Parse($Matches.Boolean) }
                '^\d+$'                               { $Value = $Value -as [int] }
                default                               {  }
            }

            $Value
        }
    }
}

AfterEachFeature {
    $Module = Get-Module -Name "My-Module"
    if ($Module) {
        $Module.Name | Remove-Module
    }

    if (Test-Path "Function:\global:ConvertTo-Parameter") {
        Remove-Item Function:\global:ConvertTo-Parameter
    }
}

Given "a test function" {
    param ([string]$func)

    Invoke-Expression $func

    $f = Get-Item 'Function:\Test'
    $f | Should -Not -BeNull
}

Given "the value '(?<Value>.*)' is provided for the '(?<Parameter>\S*)' parameter" {
    param(
        [object]$Value,
        [string]$Parameter
    )

    $Value = ConvertFrom-TableCellValue $Value
    if ($Value -ne "<No value>") {
        $Context = @{ $Parameter = $Value }
    } else {
        $Context = @{}
    }
}

When "'(?<CmdletName>.*)' is invoked" {
    [CmdletBinding()]
    Param ([string]$CmdletName)

    # NOTE: I probably don't need the if block, but I added this during debugging of this issue...just-in-case.
    # NOTE: The test fails in this step definition at this point when it executes the `Test` function which calls Get-ParameterValues.
    if ($Context.Keys.Count -gt 0) {
        $actual = &$CmdletName @Context
    } else {
        $actual = &$CmdletName
    }

    Write-Debug $actual.GetType().FullName
}

Then "the hashtable '(?<ContainsAKey>(does not contain|contains))' a key for the parameter" {
    param([string]$ContainsAKey)

    ($Context.ContainsKey($Parameter) -and $actual.ContainsKey($Context.$Parameter)) | Should -Be ($ContainsAKey -eq 'contains')
}

Sample Test Run Output

psake version 4.7.4
Copyright (c) 2010-2017 James Kovacs & Contributors

Executing InstallDependencies
Executing Init
Executing Test
Testing all features in 'C:\src\git\My-Module\Specs\features' with tags: 'Get-ParameterValues'

Feature: Get-ParameterValues
       As a PowerShell function author
       In order to more easily deal with function parameters
       I want to be able to automatically populate a dictionary
       containing the supplied function arguments or the parameter's
       default value if the argument was not supplied
       So that I can reduce boilerplate code when dealing with
       function parameters.

  Scenario: Get-ParameterValues returns a dictionary with the expected key and value
  Examples:Examples using parameter ValueWithNoDefault
    [+] Given a test function 59ms
    [+] Given the value '<No value>' is provided for the 'ValueWithNoDefault' parameter 187ms
    [-] When 'global:Test' is invoked 48ms
      at <ScriptBlock>, C:\src\git\My-Module\Public\Utility\Get-ParameterValues.ps1: line 74
      74:     $BoundParameters = Get-Variable -Scope 1 -Name PSBoundParameters -ValueOnly

      From C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 35
      Cannot find a variable with the name 'PSBoundParameters'.
    [-] Then the hashtable 'does not contain' a key for the parameter 201ms
      at <ScriptBlock>, C:\src\git\My-Module\Specs\features\steps\Utility\Get-ParameterValues.steps.ps1: line 38
      38:     ($Context.ContainsKey($Parameter) -and $actual.ContainsKey($Context.$Parameter)) | Should -Be ($ContainsAKey -eq 'contains')

      From C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 36
      You cannot call a method on a null-valued expression.
    [?] And the resolved parameter value is '<Does Not Exist>'. 40ms
      Could not find implementation for step!
      At And, C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 37

Can anyone please tell me why the $PSBoundParameters dictionary does not exist inside the Get-ParameterValues function even though parameters are passed to the Test function? (I have a feeling it may have something to do with PowerShell scoping rules, since the When script block has "script" scope--which I tried to overcome by making sure that the Test function was declared in the global scope.)

Update

So, going down the path of this might be a PowerShell scope issue that I just don't understand, I modified my Gherkin Given step as follows:

Given a test function
      """
      function global:Test {
          [CmdletBinding()]
          param(
              [Parameter(Position = 0)]
              $ValueWithNoDefault,
              [Parameter(Position = 1)]
              $ValueWithDefault = 'default',
              [Parameter(Position = 2)]
              $ValueWithNullDefault = $null
          )

          $DebugPreference = 'Continue'
          if ($PSBoundParameters) {
              Write-Debug "`$PSBoundParameters = $(ConvertTo-JSON $PSBoundParameters)"
          } else {
              Write-Debug "Unable to find `$PSBoundParameters automatic variable."
          }
          $Parameters = (Get-ParameterValues)
          $Parameters
      }
      """

And here is the output from running the tests for the first two table entries (see the original feature file above):

psake version 4.7.4
Copyright (c) 2010-2017 James Kovacs & Contributors

Executing InstallDependencies
Executing Init
Executing Test
Testing all features in 'C:\src\git\My-Module\Specs\features' with tags: 'Get-ParameterValues'

Feature: Get-ParameterValues
       As a PowerShell function author
       In order to more easily deal with function parameters
       I want to be able to automatically populate a dictionary
       containing the supplied function arguments or the parameter's
       default value if the argument was not supplied
       So that I can reduce boilerplate code when dealing with
       function parameters.

  Scenario: Get-ParameterValues returns a dictionary with the expected key and value
  Examples:Examples using parameter ValueWithNoDefault
    [+] Given a test function 54ms
DEBUG: $Parameter: ValueWithNoDefault
DEBUG: $Value: <No value>
    [+] Given the value '<No value>' is provided for the 'ValueWithNoDefault' 
parameter 29ms
DEBUG: Cmdlet: Test
DEBUG: Context: {

}
DEBUG: $PSBoundParameters = {

}
    [-] When 'Test' is invoked 47ms
      at <ScriptBlock>, C:\src\git\My-Module\Public\Utility\Get-ParameterValues.ps1: line 74
      74:     $BoundParameters = Get-Variable -Scope 1 -Name PSBoundParameters -ValueOnly

      From C:\src\git\My-Module\Specs\features\Utility\Get- ParameterValues.feature: line 41
      Cannot find a variable with the name 'PSBoundParameters'.
DEBUG: Context: {

}
DEBUG: actual:
    [+] Then the hashtable 'does not contain' a key for the parameter 61ms
    [?] And the resolved parameter value is '<Does Not Exist>'. 30ms
      Could not find implementation for step!
      At And, C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 43
DEBUG: $Parameter: ValueWithNoDefault
DEBUG: $Value:
    [+] Given the value '$null' is provided for the 'ValueWithNoDefault' parameter 14ms
DEBUG: Cmdlet: Test
DEBUG: Context: {
    "ValueWithNoDefault":  ""
}
DEBUG: $PSBoundParameters = {
    "ValueWithNoDefault":  ""
}
    [-] When 'Test' is invoked 9ms
      at <ScriptBlock>, C:\src\git\My-Module\Public\Utility\Get-ParameterValues.ps1: line 74
      74:     $BoundParameters = Get-Variable -Scope 1 -Name PSBoundParameters -ValueOnly

      From C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 41
      Cannot find a variable with the name 'PSBoundParameters'.
DEBUG: Context: {
    "ValueWithNoDefault":  ""
}
DEBUG: actual:
    [-] Then the hashtable 'contains' a key for the parameter 20ms
      at <ScriptBlock>, C:\src\git\My- Module\Specs\features\steps\Utility\Get-ParameterValues.steps.ps1: line 39
      39:     ($Context.ContainsKey('Parameter') -and 
$actual.ContainsKey($Context.$Parameter)) | Should -Be ($ContainsAKey -eq 'contains')
      From C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 42
      Expected $true, but got $false.
    [?] And the resolved parameter value is '`$null'. 5ms
      Could not find implementation for step!
      At And, C:\src\git\My-Module\Specs\features\Utility\Get-ParameterValues.feature: line 43

As shown, I "dumped" the $PSBoundParameters collection from inside the Test function created in the Given step only if it is non-null/empty/is "truthy" (otherwise I would write a string saying it doesn't exist). In every case, even when $null is assigned to the parameter, $PSBoundParameters is defined and exists inside of the Test function. However, once the call to Get-ParameterValues is made, inside of that function an attempt is made to retrieve the $PSBoundParameters variable from the parent scope (in this case, the Test function—which has already been established that the variable exists in that scope). But yet, I get the error message from the Pester test step definition. Unfortunately, these results confuse me even more.

fourpastmidnight
  • 4,032
  • 1
  • 35
  • 48

0 Answers0