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.
$args
inside a script block serving as a ScriptMethod ETS member works as expected even from inside a function: it refers to whatever (unbound) arguments are passed by the caller on invocation.
param(...)
block to formally declare parameters for the expected arguments; e.g., Add-Member -MemberType ScriptMethod ... -Value { param($Binding) AddMock -BindingParameter $Binding } -Force
. However, note that manual checks are required to ensure that all mandatory parameters receive arguments on invocation and that no extra, unexpected arguments are passed; e.g., if (-not $PSBoundParameter.ContainsKey('binding')) { throw "Missing -Binding argument" }
and if ($args.Count) { throw "Unexpected argument(s) passed: $args" }
)[1]One problem is the attempt to construct a [pscustomobject]
from an array, as Santiago points out:
You can only meaningfully use a [pscustomobject]
cast on a (single, potentially ordered) hashtable; with all other types, this cast is a virtual no-op (it creates a mostly invisible [psobject]
wrapper).[2]
You don't actually need a [pscustomobject]
instance in order to attach a ScriptMethod member to your array - just attach it to the array itself.
The bigger problem is that the ScriptMethod you added to the array is lost due to auto-enumeration when return $bindingsObject
is executed (given that you're in effect outputting an array rather than a [pscustomobject]
): it isn't the array itself that is output, but its elements, one by one, and $bindingsObject = ...
collects them in a new array; you can use return , $bindingsObject
to avoid that (or, more verbosely, but conceptually more clearly, Write-Output -NoEnumerate $bindingsObject
) or use Add-Member -PassThru
to directly output the decorated array as a whole to the pipeline, as shown below.[3]
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.