powershellmocking

Using $args in Add-Member inside a function


I'm trying to test a PowerShell function that sets the bindings for an IIS website. For the tests I want to create a Site object, that represents an existing website, with a Bindings property.

I'm using the following code to create a Site object and its Bindings:

function RemoveMock ($BindingParameter) { }

$existingSiteBindingsArray = @(
        @{ Protocol = 'http'; BindingInformation = '*:4000:' }
        @{ Protocol = 'https'; BindingInformation = '*:5000:' }
    )

$bindingsObject = [PSCustomObject]$existingSiteBindingsArray

Add-Member -InputObject $bindingsObject -MemberType ScriptMethod -Name Remove `
    -Value { RemoveMock -BindingParameter $args[0] } -Force

$websiteProperties = @{ Name = 'MyWebsite'; Bindings = $bindingsObject }
$existingSiteObject = New-MockObject -Type 'Microsoft.Web.Administration.Site' `
    -Properties $websiteProperties

This seems to work. However, I'd like to avoid repeating all this code for every test. So I would like to modify the code to use a function to get the test Bindings:

function RemoveMock ($BindingParameter) { }

function GetBindingsObject([array]$BindingsArray)
{
    $bindingsObject = [PSCustomObject]$BindingsArray
    Add-Member -InputObject $bindingsObject -MemberType ScriptMethod -Name Remove `
        -Value { RemoveMock -BindingParameter $args[0] } -Force

    return $bindingsObject
}
    
$existingSiteBindingsArray = @(
        @{ Protocol = 'http'; BindingInformation = '*:4000:' }
        @{ Protocol = 'https'; BindingInformation = '*:5000:' }
    )

$bindingsObject = GetBindingsObject -BindingsArray $existingSiteBindingsArray

$websiteProperties = @{ Name = 'MyWebsite'; Bindings = $bindingsObject }
$existingSiteObject = New-MockObject -Type 'Microsoft.Web.Administration.Site' `
    -Properties $websiteProperties

This doesn't work because the $args[0] picks up the argument passed into the function, rather than the argument passed into the mocked Remove method.

Is there any way to specify the argument(s) passed into a script method added via Add-Member if Add-Member is called within a function?

(note that although the modified code is longer than the original, the function will be called repeatedly for multiple tests, and in reality the function will be longer, saving more lines per test)

EDIT: Replaced AddMock with RemoveMock for consistency. Added RemoveMock function definition.


Solution


  • Therefore:

    function GetBindingsObject([array]$BindingsArray)
    {
        # Decorate the array with a ScriptMethod and write it to the pipeline *as a whole*.
        Add-Member -PassThru -InputObject $BindingsArray -MemberType ScriptMethod -Name Remove `
            -Value { AddMock -BindingParameter $args[0] } -Force
    }
    
    $existingSiteBindingsArray = @(
            @{ Protocol = 'http'; BindingInformation = '*:4000:' }
            @{ Protocol = 'https'; BindingInformation = '*:5000:' }
        )
    
    $bindingsObject = GetBindingsObject -BindingsArray $existingSiteBindingsArray
    
    $websiteProperties = @{ Name = 'MyWebsite'; Bindings = $bindingsObject }
    $existingSiteObject = New-MockObject -Type 'Microsoft.Web.Administration.Site' `
        -Properties $websiteProperties
    

    [1] Note that the usual automatic techniques do not work in the case of a ScriptMethod ETS member: [Parameter(Mandatory)] attributes are ignored, and so is [CmdletBindingAttribute()]; similarly, the automatic enforcement of passing only arguments that bind to declared parameters doesn't work (use of either attribute normally makes a script block / function / script an advanced one, which normally implies this behavior).

    [2] For more information on this problematic behavior, see this answer.

    [3] See this answer for more information.